Analyzed GitHub issue and related code.
UnderstandUpdated documentation with correct URL format.
EditCommented on issue explaining user error and fix.
VerifyAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/956
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
The user wants me to analyze GitHub issue #956. Let me fetch the issue details first.
>gh issue view 956 --repo badlogic/pi-mono --json title,body,comments,labels,state{"body":"### What happened?\n\n```\nError: 401 Attempted to access resource with incorrect regional hostname. Please make your request to us.api.openai.com\n```\n\n### Steps to reproduce\n\nUse an api key from an openai project that has US data residency.\n\n### Expected behavior\n\n_No response_\n\n### Version\n\n0.49.3","comments":[{"id":"IC_kwDOPbFNk87iYIgy","author":{"login":"tekumara"},"authorAssociation":"NONE","body":"Tried setting:\n```\n❯ cat ~/.pi/agent/models.json\n{\n \"providers\": {\n \"openai\": {\n \"baseUrl\": \"https://us.api.openai.com\"\n }\n }\n}\n```\n\nBut get an empty response now instead of a 401:\n\n```\n❯ pi\n\n pi v0.49.3\n escape to interrupt\n ctrl+c to clear\n ctrl+c twice to exit\n ctrl+d to exit (empty)\n ctrl+z to suspend\n ctrl+k to delete to end\n shift+tab to cycle thinking\n ctrl+p/shift+ctrl+p to cycle models\n ctrl+l to select model\n ctrl+o to expand tools\n ctrl+t to toggle thinking\n ctrl+g for external editor\n / for commands\n ! to run bash\n !! to run bash (no context)\n alt+enter to queue follow-up\n alt+up to edit all queued messages\n ctrl+v to paste image\n drop files to attach\n\n\n\n hi\n\n\n\n itle></head>\n\n /center>\n\n\n\n\n\n────────────────────────────────────────────────────────────────────────\n\n────────────────────────────────────────────────────────────────────────\n~/code/ (main)\n0.0%/400k (auto) gpt-5.2-codex\n```","createdAt":"2026-01-26T05:42:51Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/956#issuecomment-3797977138","viewerDidAuthor":false}],"labels":[{"id":"LA_kwDOPbFNk88AAAACHWTBxw","name":"bug","description":"Something isn't working","color":"d73a4a"}],"state":"OPEN","title":"openai: Error: 401 Attempted to access resource with incorrect regional hostname"}Let me understand the issue:
us.api.openai.com for US data residency)us.api.openai.combaseUrl in ~/.pi/agent/models.json, but that resulted in an empty/garbage responseI need to:
Let me look at the relevant code files.
Let me read the relevant OpenAI provider code and understand how base URLs are handled:
>find /Users/badlogic/workspaces/pi-mono/packages/ai -name "*.ts" | xargs rg -l "openai" --ignore-case | head -20/Users/badlogic/workspaces/pi-mono/packages/ai/test/openai-completions-tool-choice.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/openai-completions-tool-result-images.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/image-tool-result.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/azure-utils.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/openai-responses-reasoning-replay-e2e.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/cross-provider-handoff.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/total-tokens.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/abort.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/xhigh.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/empty.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/tool-call-without-result.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/unicode-surrogate.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/tool-call-id-normalization.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/stream.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/context-overflow.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/dist/models.d.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/test/openai-codex-stream.test.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/dist/models.generated.d.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/dist/providers/azure-openai-responses.d.ts
/Users/badlogic/workspaces/pi-mono/packages/ai/dist/types.d.ts>find /Users/badlogic/workspaces/pi-mono/packages/coding-agent -name "*.ts" | xargs rg -l "baseUrl\|models\.json" 2>/dev/null | head -10(no output)Let me look at the OpenAI provider implementation and the models configuration.
>rg -l "models\.json" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src --type ts/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/config.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/model-registry.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/auth-storage.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/core/sdk.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.tsI see that the OpenAI client uses model.baseUrl directly. Let me look at how models are configured and how baseUrl is set from the config file.
I can see the issue now. Looking at the code flow:
In model-registry.ts, when loading custom models from models.json, the loadBuiltInModels function applies the override's baseUrl to models.
The issue is the user is setting:
{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}This is an "override-only" config (no custom models defined). Looking at loadBuiltInModels:
const override = overrides.get(provider);if (!override) return models;
// Apply baseUrl/headers override to all models of this providerconst resolvedHeaders = resolveHeaders(override.headers);return models.map((m) => ({ ...m, baseUrl: override.baseUrl ?? m.baseUrl, headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,}));This looks correct. Let me check how the OpenAI client uses model.baseUrl in openai-responses.ts:
return new OpenAI({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders: headers,});The problem is that model.baseUrl is being used, but for OpenAI, there's a default baseUrl that is probably https://api.openai.com/v1. But when the user sets https://us.api.openai.com, they're missing the /v1 path segment!
Let me verify the OpenAI models to see what their default baseUrl is:
Let me check the OpenAI models configuration to see what the default baseUrl looks like:
>rg "openai.*baseUrl|baseUrl.*openai" /Users/badlogic/workspaces/pi-mono/packages/ai/src -A 2 -B 2/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-completions",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "groq",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.groq.com/openai/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: false,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],
--
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- api: "openai-responses",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- provider: "openai",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts: baseUrl: "https://api.openai.com/v1",
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- reasoning: true,
/Users/badlogic/workspaces/pi-mono/packages/ai/src/models.generated.ts- input: ["text", "image"],I found the issue. The default OpenAI baseUrl is https://api.openai.com/v1. When the user sets baseUrl to https://us.api.openai.com, they're missing the /v1 path.
The user should be using https://us.api.openai.com/v1 instead of https://us.api.openai.com.
The "empty response" the user got when they set the wrong URL is likely because they hit OpenAI's homepage or another endpoint that returns HTML (not JSON). The itle></head> and /center> snippets in the output appear to be fragments of an HTML response being incorrectly interpreted.
There are a couple of solutions:
Document the correct URL format: Users need to include /v1 in their baseUrl.
Auto-append /v1: This could be error-prone since some OpenAI-compatible APIs might not use /v1.
Better error handling: The streaming code should detect when it receives HTML instead of JSON and provide a clearer error message.
Let me verify this is the issue by checking what the OpenAI SDK does with baseURL:
Now I understand the issue. Let me verify the OpenAI SDK behavior and check if there's any documentation for the models.json format:
>rg -l "models\.json" /Users/badlogic/workspaces/pi-mono/packages/coding-agent --include="*.md"rg: unrecognized flag --include
similar flags that are available: --include-zero
Command exited with code 2>rg "baseUrl" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/README.md -A 5 -B 5(no output)
Command exited with code 1Let me search for where models.json is documented and also look at environment variables that might be relevant.
>rg "models\.json" /Users/badlogic/workspaces/pi-mono/packages/coding-agent -g "*.md"/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/providers.md:**Via models.json:** Add Ollama, LM Studio, vLLM, or any provider that speaks a supported API (OpenAI Completions, OpenAI Responses, Anthropic Messages, Google Generative AI). See [models.md](models.md).
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/providers.md:4. Custom provider keys from `models.json`
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/README.md:**Custom providers & models:** Add providers via `~/.pi/agent/models.json` if they speak a supported API (OpenAI, Anthropic, Google). For custom APIs or OAuth, use extensions. See [docs/models.md](docs/models.md) and [docs/custom-provider.md](docs/custom-provider.md).
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- `models.json` headers can resolve environment variables and shell commands. See [docs/models.md#value-resolution](docs/models.md#value-resolution).
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- Header values in `models.json` now resolve environment variables (if a header value matches an env var name, the env var value is used). This may change behavior if a literal header value accidentally matches an env var name. ([#909](https://github.com/badlogic/pi-mono/issues/909))
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- Header values in `models.json` now support environment variables and shell commands, matching `apiKey` resolution ([#909](https://github.com/badlogic/pi-mono/issues/909))
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- Removed `strictResponsesPairing` compat option from models.json schema (no longer needed)
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- API keys in `models.json` can now be retrieved via shell command using `!` prefix (e.g., `"apiKey": "!security find-generic-password -ws 'anthropic'"` for macOS Keychain) ([#762](https://github.com/badlogic/pi-mono/pull/762) by [@cv](https://github.com/cv))
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- Built-in provider overrides in `models.json`: override just `baseUrl` to route a built-in provider through a proxy while keeping all its models, or define `models` to fully replace the provider ([#406](https://github.com/badlogic/pi-mono/pull/406) by [@yevhen](https://github.com/yevhen))
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:`ModelRegistry` is a new class that manages model discovery and API key resolution. It combines built-in models with custom models from `models.json` and resolves API keys via `AuthStorage`.
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:const modelRegistry = discoverModels(authStorage); // + ~/.pi/agent/models.json
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- **OpenAI compatibility overrides in models.json**: Custom models using `openai-completions` API can now specify a `compat` object to override provider quirks (`supportsStore`, `supportsDeveloperRole`, `supportsReasoningEffort`, `maxTokensField`). Useful for LiteLLM, custom proxies, and other non-standard endpoints. ([#133](https://github.com/badlogic/pi-mono/issues/133), thanks @fink-andreas for the initial idea and PR)
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- **`authHeader` option in models.json**: Custom providers can set `"authHeader": true` to automatically add `Authorization: Bearer <apiKey>` header. Useful for providers that require explicit auth headers. ([#81](https://github.com/badlogic/pi-mono/issues/81))
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/CHANGELOG.md:- Add custom model configuration via `~/.pi/models.json`
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/models.md:Add custom providers and models (Ollama, vLLM, LM Studio, proxies) via `~/.pi/agent/models.json`.
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/sdk.md:- Custom models (`models.json`)
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/sdk.md:// Find any model by provider/id, including custom models from models.json
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/sdk.md:4. Fallback resolver (for custom provider keys from `models.json`)
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/sdk.md:// Default: uses ~/.pi/agent/auth.json and ~/.pi/agent/models.json
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/sdk.md:const customRegistry = new ModelRegistry(customAuth, "/my/app/models.json");
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/sdk.md:// No custom models.json (built-in models only)
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/sdk.md:// Model registry (no custom models.json)>rg "OPENAI_BASE_URL|OPENAI_API_BASE" /Users/badlogic/workspaces/pi-mono/packages --type ts/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/cli/args.ts: AZURE_OPENAI_BASE_URL - Azure OpenAI base URL (https://{resource}.openai.azure.com/openai/v1)
/Users/badlogic/workspaces/pi-mono/packages/pods/src/commands/models.ts: console.log(chalk.gray(`export OPENAI_BASE_URL="http://${host}:${port}/v1"`));
/Users/badlogic/workspaces/pi-mono/packages/pods/src/commands/models.ts: curl $OPENAI_BASE_URL/chat/completions \\
/Users/badlogic/workspaces/pi-mono/packages/ai/src/providers/azure-openai-responses.ts: const baseUrl = options?.azureBaseUrl?.trim() || process.env.AZURE_OPENAI_BASE_URL?.trim() || undefined;
/Users/badlogic/workspaces/pi-mono/packages/ai/src/providers/azure-openai-responses.ts: "Azure OpenAI base URL is required. Set AZURE_OPENAI_BASE_URL or AZURE_OPENAI_RESOURCE_NAME, or pass azureBaseUrl, azureResourceName, or model.baseUrl.",
/Users/badlogic/workspaces/pi-mono/packages/ai/test/azure-utils.ts: const hasBaseUrl = !!(process.env.AZURE_OPENAI_BASE_URL || process.env.AZURE_OPENAI_RESOURCE_NAME);Let me look at the docs/models.md to understand the documented format and see if there's already a way to override the OpenAI base URL or if this needs to be added.
Now I see the issue clearly. The documentation shows this example for overriding a built-in provider:
{ "providers": { "anthropic": { "baseUrl": "https://my-proxy.example.com/v1" } }}So when the user tried:
{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}The problem is they forgot the /v1 path. The correct value should be:
{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com/v1" } }}This is documented correctly (the examples all show /v1 suffix), but the user missed it.
The second issue is that when the wrong URL is used (https://us.api.openai.com without /v1), the user gets garbage output (itle></head> and /center>) instead of a clear error. This is because the OpenAI SDK is hitting an HTML page and the code isn't properly detecting/handling that the response is HTML instead of JSON.
Let me verify this by looking at how the streaming response is processed.
Let me check how the OpenAI response streaming handles error cases:
Looking at the code, the issue is that when the user sets baseUrl to https://us.api.openai.com (without /v1), the OpenAI SDK likely hits an HTML page (like a landing page or error page). When the SDK tries to parse this as a streaming response, it gets garbage.
The error handling is present in the processResponsesStream function for event.type === "error" and event.type === "response.failed", but if the response is HTML, it never generates proper SSE events. Instead, the OpenAI SDK might be trying to process garbage as SSE data.
Let me check what the issue reporter saw. They got garbage output that looks like HTML fragments (itle></head> and /center>). This is because:
https://us.api.openai.com (without /v1) returns HTMLThe root cause is that the user needs to use https://us.api.openai.com/v1 (with /v1).
But we should consider:
/v1baseUrl doesn't end with /v1, we could warn or auto-fixLet me see if there's an environment variable for OpenAI base URL that we should support:
>rg "OPENAI" /Users/badlogic/workspaces/pi-mono/packages/ai/src/env-api-keys.ts* Get API key for provider from known environment variables, e.g. OPENAI_API_KEY.
openai: "OPENAI_API_KEY",
"azure-openai-responses": "AZURE_OPENAI_API_KEY",Now I have the full picture. Let me analyze this issue:
Issue: OpenAI now supports regional API endpoints (e.g., us.api.openai.com for US data residency). Users with US data residency API keys get a 401 error when hitting the default api.openai.com.
User's attempt: The user tried to override the OpenAI baseUrl via ~/.pi/agent/models.json:
{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}Problems:
The user forgot the /v1 path suffix. OpenAI models have baseUrl: "https://api.openai.com/v1", so the override should be https://us.api.openai.com/v1.
When using the wrong URL (without /v1), the response is HTML instead of SSE data. The streaming code doesn't detect this and just passes along garbage text.
There are a few approaches:
Just update docs/models.md to explicitly mention the /v1 suffix for OpenAI regional endpoints. This is the simplest but least user-friendly.
OpenAI's official SDK already supports OPENAI_BASE_URL environment variable. Adding support for this would let users set OPENAI_BASE_URL=https://us.api.openai.com/v1 in their environment.
Detect when a response is HTML instead of SSE data and throw a meaningful error like "Received HTML instead of API response. Check your baseUrl configuration."
/v1 when missingThis is risky because some OpenAI-compatible APIs might not use /v1.
I think combining Options B and C makes the most sense. Adding environment variable support for OPENAI_BASE_URL aligns with how the official SDK works and gives users a cleaner way to configure regional endpoints. Simultaneously, improving error detection to catch HTML responses and surface a helpful message would prevent silent failures when configuration is wrong. The implementation would involve updating the OpenAI provider to check for the environment variable first, then fall back to model configuration, and adding validation in the response handler to identify and report malformed responses.
Let me check the OpenAI completions provider as well to see how it handles baseURL:
I see that both openai-responses.ts and openai-completions.ts use model.baseUrl directly when creating the OpenAI client:
return new OpenAI({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders: headers,});```
The issue is clear:
1. User sets `baseUrl: "https://us.api.openai.com"` (missing `/v1`)2. The OpenAI SDK hits `https://us.api.openai.com/chat/completions` (or similar) which returns HTML (404 or landing page)3. The SDK tries to parse this as SSE data, resulting in garbage output
## Summary of Analysis
**Issue**: OpenAI has regional endpoints (`us.api.openai.com`) for US data residency. Users with US data residency API keys get 401 errors on the default global endpoint.
**User's attempted fix**: Setting `baseUrl: "https://us.api.openai.com"` in models.json, which is **missing the `/v1` suffix**.
**Result**: The request hits an HTML page instead of the API, producing garbage output.
**Root cause**: User error (missing `/v1`), but the documentation doesn't explicitly mention OpenAI regional endpoints or the requirement for the `/v1` suffix when overriding OpenAI's baseUrl.
## Proposed Fix
1. **Documentation update** in `docs/models.md`: Add a specific example for OpenAI regional endpoints showing the correct URL format (`https://us.api.openai.com/v1`).
2. **(Optional) Add `OPENAI_BASE_URL` environment variable support**: This is the standard way the official OpenAI SDK supports custom base URLs. Users could then just set `OPENAI_BASE_URL=https://us.api.openai.com/v1` without needing models.json.
3. I could also add validation to detect HTML responses—if the response starts with `<!DOCTYPE` or `<html`, throw a descriptive error message pointing users to verify their baseUrl includes `/v1`. This would catch the issue immediately rather than producing garbled output.
## Files to Modify
For the minimal fix (documentation only):- `packages/coding-agent/docs/models.md`: Add OpenAI regional endpoint example
For a more complete solution:- `packages/ai/src/providers/openai-responses.ts`: Check `OPENAI_BASE_URL` env var and add HTML detection- `packages/ai/src/providers/openai-completions.ts`: Apply the same changes- `packages/coding-agent/src/cli/args.ts`: Document the `OPENAI_BASE_URL` variable- `packages/ai/README.md`: Add documentation for `OPENAI_BASE_URL`- `packages/coding-agent/docs/models.md`: Include regional endpoint examplesUsers with OpenAI projects configured for US data residency get a 401 error:
"Attempted to access resource with incorrect regional hostname. Please make your request to us.api.openai.com"
The user attempted to fix this by setting baseUrl in models.json, but used the wrong URL format.
User error with missing path suffix. The user configured:
{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}But the correct configuration requires the /v1 path:
{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com/v1" } }}OpenAI models have baseUrl: "https://api.openai.com/v1" by default. When the user overrides without /v1, the OpenAI SDK hits https://us.api.openai.com/chat/completions which returns an HTML error page. The streaming code then displays HTML fragments as text (the itle></head> and /center> garbage the user saw).
Two-part solution:
1. Documentation update (essential): Add explicit example for OpenAI regional endpoints in docs/models.md
2. Environment variable support (nice-to-have): Support OPENAI_BASE_URL like the official OpenAI SDK does, allowing users to set OPENAI_BASE_URL=https://us.api.openai.com/v1 as an alternative to models.json
| File | Change |
|---|---|
packages/coding-agent/docs/models.md | Add "OpenAI Regional Endpoints" section with correct example |
packages/ai/src/providers/openai-responses.ts | Check OPENAI_BASE_URL env var in createClient() |
packages/ai/src/providers/openai-completions.ts | Check OPENAI_BASE_URL env var in createClient() |
packages/coding-agent/src/cli/args.ts | Document OPENAI_BASE_URL in env vars section |
packages/ai/README.md | Document OPENAI_BASE_URL environment variable |
For openai-responses.ts and openai-completions.ts, modify createClient():
return new OpenAI({ apiKey, baseURL: model.baseUrl || process.env.OPENAI_BASE_URL, dangerouslyAllowBrowser: true, defaultHeaders: headers,});Actually, the logic should be: env var overrides model.baseUrl when provider is "openai":
const baseURL = model.provider === "openai" && process.env.OPENAI_BASE_URL ? process.env.OPENAI_BASE_URL : model.baseUrl;For docs/models.md, add under "Overriding Built-in Providers":
### OpenAI Regional Endpoints
For OpenAI projects with data residency requirements (US, EU), override the base URL.Note: The `/v1` path suffix is required.
```json{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com/v1" } }}Or set the environment variable: OPENAI_BASE_URL=https://us.api.openai.com/v1
just update docs, then comment on the issue telling the user what they did wrong and how to fix it
The user wants me to:
docs/models.md to add information about OpenAI regional endpointsLet me do both.
Now I'll update the "Overriding Built-in Providers" section to add information about OpenAI regional endpoints.
Now let me update the Table of Contents to include the new section.
Now I'll run the check to make sure I didn't break anything.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
Checked 443 files in 275ms. No fixes applied.
> @mariozechner/pi-web-ui@0.50.1 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 36ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.Checks pass. Now let me comment on the GitHub issue.
Now let me comment on the issue:
>gh issue comment 956 --repo badlogic/pi-mono --body 'The issue is the missing `/v1` path suffix in your `baseUrl`. OpenAI models use `https://api.openai.com/v1` by default, so your override needs to include it too:
```json
{
"providers": {
"openai": {
"baseUrl": "https://us.api.openai.com/v1"
}
}
}
```
Without `/v1`, the SDK hits an HTML error page instead of the API, which explains the garbage output you saw.
Updated the docs to make this clearer: https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/models.md#openai-regional-endpoints'https://github.com/badlogic/pi-mono/issues/956#issuecomment-3814679542Done. I've:
Should I also close the issue since it's essentially a documentation/user-error issue that's now documented? Let me ask the user or wait for them to verify it works.
Done. I've:
Updated docs/models.md: Added "OpenAI Regional Endpoints" subsection under "Overriding Built-in Providers" with the correct URL format (https://us.api.openai.com/v1)
Commented on issue #956: Explained that the missing /v1 suffix caused the SDK to hit an HTML page instead of the API, with the correct configuration example
{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}const override = overrides.get(provider);if (!override) return models;
// Apply baseUrl/headers override to all models of this providerconst resolvedHeaders = resolveHeaders(override.headers);return models.map((m) => ({ ...m, baseUrl: override.baseUrl ?? m.baseUrl, headers: resolvedHeaders ? { ...m.headers, ...resolvedHeaders } : m.headers,}));return new OpenAI({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders: headers,});{ "providers": { "anthropic": { "baseUrl": "https://my-proxy.example.com/v1" } }}{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com/v1" } }}{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}return new OpenAI({ apiKey, baseURL: model.baseUrl, dangerouslyAllowBrowser: true, defaultHeaders: headers,});```
The issue is clear:
1. User sets `baseUrl: "https://us.api.openai.com"` (missing `/v1`)2. The OpenAI SDK hits `https://us.api.openai.com/chat/completions` (or similar) which returns HTML (404 or landing page)3. The SDK tries to parse this as SSE data, resulting in garbage output
## Summary of Analysis
**Issue**: OpenAI has regional endpoints (`us.api.openai.com`) for US data residency. Users with US data residency API keys get 401 errors on the default global endpoint.
**User's attempted fix**: Setting `baseUrl: "https://us.api.openai.com"` in models.json, which is **missing the `/v1` suffix**.
**Result**: The request hits an HTML page instead of the API, producing garbage output.
**Root cause**: User error (missing `/v1`), but the documentation doesn't explicitly mention OpenAI regional endpoints or the requirement for the `/v1` suffix when overriding OpenAI's baseUrl.
## Proposed Fix
1. **Documentation update** in `docs/models.md`: Add a specific example for OpenAI regional endpoints showing the correct URL format (`https://us.api.openai.com/v1`).
2. **(Optional) Add `OPENAI_BASE_URL` environment variable support**: This is the standard way the official OpenAI SDK supports custom base URLs. Users could then just set `OPENAI_BASE_URL=https://us.api.openai.com/v1` without needing models.json.
3. I could also add validation to detect HTML responses—if the response starts with `<!DOCTYPE` or `<html`, throw a descriptive error message pointing users to verify their baseUrl includes `/v1`. This would catch the issue immediately rather than producing garbled output.
## Files to Modify
For the minimal fix (documentation only):- `packages/coding-agent/docs/models.md`: Add OpenAI regional endpoint example
For a more complete solution:- `packages/ai/src/providers/openai-responses.ts`: Check `OPENAI_BASE_URL` env var and add HTML detection- `packages/ai/src/providers/openai-completions.ts`: Apply the same changes- `packages/coding-agent/src/cli/args.ts`: Document the `OPENAI_BASE_URL` variable- `packages/ai/README.md`: Add documentation for `OPENAI_BASE_URL`- `packages/coding-agent/docs/models.md`: Include regional endpoint examples{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com" } }}{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com/v1" } }}return new OpenAI({ apiKey, baseURL: model.baseUrl || process.env.OPENAI_BASE_URL, dangerouslyAllowBrowser: true, defaultHeaders: headers,});const baseURL = model.provider === "openai" && process.env.OPENAI_BASE_URL ? process.env.OPENAI_BASE_URL : model.baseUrl;### OpenAI Regional Endpoints
For OpenAI projects with data residency requirements (US, EU), override the base URL.Note: The `/v1` path suffix is required.
```json{ "providers": { "openai": { "baseUrl": "https://us.api.openai.com/v1" } }}