Skip to content

Add OpenAI-compatible model provider - #167

Draft
ndisidore wants to merge 3 commits into
mainfrom
feat/openai-compatible-provider
Draft

Add OpenAI-compatible model provider#167
ndisidore wants to merge 3 commits into
mainfrom
feat/openai-compatible-provider

Conversation

@ndisidore

@ndisidore ndisidore commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Adds beta support for user-configured OpenAI-compatible Chat Completions providers (openai-compatible in config, OpenAI-compatible in the UI).

This first version is intentionally minimal:

  • Direct mode only; it bypasses user unified billing and is unavailable in platform AI Gateway mode.
  • Requires a model ID, bearer token, HTTPS base URL, context window, and output limit.
  • Rejects URL credentials, query strings, fragments, and invalid token limits. Pasted /chat/completions endpoints are normalized automatically.
  • Supports text only. Historical images become omitted-attachment markers.
  • Persists a conservative compatibility profile, using system and max_tokens while disabling optional features generic endpoints may not support. We can look at expanding this in the future

Should unlock the following services:

  • OpenRouter
  • Groq
  • DeepInfra
  • DeepSeek
  • Mistral AI
  • Moonshot AI / Kimi
  • Hugging Face Inference Providers
  • HTTPS-hosted vLLM, LiteLLM, llama.cpp, and similar self-hosted gateways
  • And a bunch more

Screenshots

Screenshot from 2026-08-12 08-56-28 Screenshot from 2026-08-12 09-05-30 Screenshot from 2026-08-12 09-05-44 Screenshot from 2026-08-12 09-06-17 Screenshot from 2026-08-12 09-07-37 Screenshot from 2026-08-12 09-08-27

@github-actions github-actions Bot added workshop/frontend Changes to the Workshop frontend kernel Changes to the Workshop kernel workshop/shared Changes to shared Workshop APIs labels Aug 12, 2026
@ask-bonk

ask-bonk Bot commented Aug 12, 2026

Copy link
Copy Markdown

gatewayNativeModel (line 183) is only reached via getModelViaUserGateway/gateway paths, which openai-compatible never reaches (short-circuited earlier). Its switch has a default returning undefined, so no exhaustiveness problem. Good — the design correctly keeps openai-compatible out of all gateway routing.

I've completed a thorough review. The change is well-structured, well-tested, and consistent with the codebase's capability/routing conventions. I found no actionable defects.

Summary of what I verified:

  • normalizeAiModelConfig validation/normalization is correct and thoroughly unit-tested (URL scheme, credentials, query/fragment, /chat/completions stripping, token-limit bounds, profile whitelist).
  • getModel short-circuits openai-compatible before both platform-gateway and user-gateway (BYOK) routing, matching the stated "direct mode only, bypasses unified billing, unavailable in gateway mode." The getModelDirect/getChatContext/addModel/listModels guards are consistent and defensive.
  • The non-null assertions on contextWindow/outputLimit are safe because every reachable path normalizes/validates first.
  • Attachment handling correctly degrades images/PDFs to text markers for the text-only profile, with the new modelSupportsChatAttachment checking resolved-model capabilities on replay.
  • Model config (with apiToken) is never returned to gadgets; only profile is listed.
  • gatewayNativeModel's switch is unaffected (openai-compatible never reaches it), so no exhaustiveness gap.
  • Frontend validation mirrors the backend, and the raw URL is intentionally normalized server-side.

LGTM!

github run

@ask-bonk

ask-bonk Bot commented Aug 12, 2026

Copy link
Copy Markdown

Confirmed: for the OpenAI provider (openai-completions api, line 644), baseUrl is https://api.openai.com/v1 — i.e., the /v1 root without /chat/completions. pi-ai appends the endpoint path. The openai-compatible normalization strips /chat/completions to produce the same shape. Consistent and correct.

The review is complete. Let me summarize my findings.

The PR is well-structured and thoroughly tested. My analysis of all 14 changed files:

  • ai-model-config.tsnormalizeAiModelConfig validation/normalization is correct; the non-null assertions are guarded by Number.isSafeInteger(...) checks that catch undefined. URL normalization (strip credentials/query/fragment//chat/completions, require HTTPS) is sound and matches the /v1-root baseUrl shape pi-ai expects (consistent with the existing openai direct path).
  • ai-models.tsgetModel correctly short-circuits openai-compatible before both platform-gateway and BYOK routing, and throws in AI Gateway mode. getModelDirect builds a text-only handle with the conservative compat profile. Assertions are safe because getModel/addModel normalize first.
  • user.tslistModels/addModel/getChatContext guards are consistent. The getChatContext refactor preserves gateway-first precedence (gwConfig?.resolveModel(modelId) ?? storedModel) and the new user-model-resolution.test.ts locks in the important property that a persisted openai-compatible model isn't silently replaced by a colliding gateway model after a mode switch.
  • chat-attachment-validation.ts / agent.tsmodelSupportsChatAttachment checks resolved-model capabilities on replay; images degrade cleanly to omitted-attachment text markers for the text-only profile.
  • agent-compaction.tsgetModelTokenLimits clamps to MAX_CUSTOM_MODEL_CONTEXT_WINDOW and reserves output correctly.
  • api.ts — new fields and MAX_CUSTOM_MODEL_CONTEXT_WINDOW are doc-commented per the shared-API bar; model config (with apiToken) is never exposed to gadgets, only profile.
  • Frontend AddModelModal.tsx — validation mirrors the backend; raw URL is intentionally normalized server-side.

I found no actionable defects. The change is consistent with the codebase's capability/routing conventions and the stated "direct mode only, unavailable in gateway mode" design.

LGTM!

github run

Comment thread packages/workshop-shared/src/api.ts Outdated
Comment thread packages/workshop-shared/src/api.ts Outdated
Comment thread packages/workshop-shared/src/api.ts Outdated

// Validate and normalize fields owned by the OpenAI-compatible provider.
export function normalizeAiModelConfig(config: AiModelConfig): AiModelConfig {
if (config.provider !== "openai-compatible") return config;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to validate openai-compatible configs but not other types of configs?

A lot of this function is redundantly validating things that capnweb-validate will already have checked for us. Can it be simplified? Or maybe it can be removed entirely? Is any of this validation actually important?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of user input here because there's not much we can infer nor do I think its worth the complexity to do so - to keep things simple I want to support but leave as an advanced escape hatch

Cap’n Web covers the wire shape, but not the semantic checks or URL normalization. We still need the conditional fields, URL checks, integer limits, and outputLimit < contextWindow. I’ll remove anything that only repeats the generated validator.

Comment thread packages/workshop-frontend/src/AddModelModal.tsx Outdated

// Pick an example model to show in the custom-model placeholders for the given provider.
function exampleModel(provider: AiModelProvider): { modelId: string, name: string } {
if (provider === 'openai-compatible') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sort of inconsistent with how ollama is handled. ollama also has no suggested model, which means it falls back to FALLBACK_EXAMPLE_MODEL, which is actually currently set specifically to be a good example for ollama.

Seems like we should extend FALLBACK_EXAMPLE_MODEL to be a full on map from provider name to an exmaple model?

Comment thread packages/workshop-backend/src/ai-model-config.ts Outdated
};
}

const OPENAI_COMPATIBLE_PROFILES = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pi seems to have some of its own code to guess at capabilities based on known hostnames, although apparently it doesn't have great defaults for hostnames that aren't in the list (it defaults to supportsDeveloperRole: true which several people complained about). It seems like it would be nice to leverage Pi's list though, rather than always assume conservative settings. Can we figure out how to do that?

@ndisidore ndisidore Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually already are using this but our conservative profile pins seven fields that generic endpoints commonly reject (which really just helps models without host entries).
If we're okay with it, we can remove compat and trust Pi completely. Known hosts improve, but unknown hosts receive optimistic OpenAI defaults.

Comment thread packages/workshop-backend/src/chat-attachment-validation.ts Outdated
maxOutputTokens,
};
}
let model = SUGGESTED_MODELS[config.provider][config.model];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh I'm realizing this whole function is kinda broken, predating this change. It doesn't work for models not in SUGGESTED_MODELS!

@AshishKumar4 Can you please figure out a better approach here? Does pi-agent-core give us any new options for figuring out a model's token limits? If not maybe we could pull from models.dev?

@ndisidore
ndisidore force-pushed the feat/openai-compatible-provider branch from 0fda37d to 8d92708 Compare August 13, 2026 18:40
@github-actions

Copy link
Copy Markdown

Thank you for taking the time to contribute. This pull request was automatically closed because:

  • The "small, concrete change" confirmation is not checked.
  • The "maintainer assessment" confirmation is not checked.
  • The "contribution guidelines" confirmation is not checked.
  • The patch changes 2184 lines; the automatic limit is 30.

Please update the pull request to meet these automatic checks, then reopen it. Passing these checks does not guarantee acceptance; maintainers still determine whether a change is obviously correct and trivially verifiable.

If an exception is appropriate, a maintainer can apply the policy/override label before reopening the pull request. See the contribution guidelines for details.

@@ -0,0 +1,616 @@
// Generated by scripts/build-openai-compatible-models.mjs. Do not edit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't have me particularly excited, but the alternative is pulling in the full pi catalog with is hundreds of thousands of entries long.
This is built as a smaller subset with only the necessary info directly from Pi at build time

@kentonv

kentonv commented Aug 21, 2026

Copy link
Copy Markdown
Member

I don't think #184 was intended to close this; silly github.

@ndisidore are you still working on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kernel Changes to the Workshop kernel policy/override workshop/frontend Changes to the Workshop frontend workshop/shared Changes to shared Workshop APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants