fix(openai): use standard chat completions API for OpenRouter compatibility#1046
fix(openai): use standard chat completions API for OpenRouter compatibility#1046NicolasArnouts wants to merge 3 commits intoItzCrazyKns:masterfrom
Conversation
…bility Replace OpenAI-exclusive APIs with standard endpoints that work across OpenAI-compatible providers (OpenRouter, LiteLLM, etc.): - generateObject: Use chat.completions.create with response_format instead of chat.completions.parse (returns 404 on OpenRouter) - streamObject: Use chat.completions.create with streaming instead of responses.stream (OpenAI Responses API not supported by other providers) Also adds shared parseJson utility for stripping markdown code fences that LLMs sometimes wrap around JSON responses even with json_object mode. Fixes ItzCrazyKns#959
There was a problem hiding this comment.
1 issue found across 3 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/lib/utils/parseJson.ts">
<violation number="1" location="src/lib/utils/parseJson.ts:21">
P2: Opening fence stripping only handles plain or `json` fences; other common language tags (```js, ```jsonc, etc.) leave the tag in the string and cause JSON parsing to fail.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const trimmed = text.trim(); | ||
| if (trimmed.startsWith('```')) { | ||
| return trimmed | ||
| .replace(/^```(?:json)?\s*/i, '') |
There was a problem hiding this comment.
P2: Opening fence stripping only handles plain or json fences; other common language tags (js, jsonc, etc.) leave the tag in the string and cause JSON parsing to fail.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/utils/parseJson.ts, line 21:
<comment>Opening fence stripping only handles plain or `json` fences; other common language tags (```js, ```jsonc, etc.) leave the tag in the string and cause JSON parsing to fail.</comment>
<file context>
@@ -0,0 +1,43 @@
+ const trimmed = text.trim();
+ if (trimmed.startsWith('```')) {
+ return trimmed
+ .replace(/^```(?:json)?\s*/i, '')
+ .replace(/```\s*$/, '')
+ .trim();
</file context>
Add check for empty content before calling repairJson to prevent "is empty" error when the model returns null/empty response.
- Add null safety checks in convertToOpenAIMessages for tool call arguments that may be strings or objects - Wrap tool call argument parsing in try-catch to handle malformed JSON gracefully, falling back to empty object on error - Add guard against undefined/empty queries in webSearch action - Add fallback for undefined chatHistory in researcher - Ensure SearXNG always returns arrays even on empty responses These fixes prevent crashes when OpenRouter/compatible providers return unexpected data formats during streaming tool calls.
There was a problem hiding this comment.
2 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/lib/models/providers/openai/openaiLLM.ts">
<violation number="1" location="src/lib/models/providers/openai/openaiLLM.ts:187">
P2: Parse failures in streaming tool calls are now swallowed and emitted as a synthetic `{}` arguments tool call, which downstream consumers execute without validation. This can trigger incorrect tool execution instead of surfacing the parse error.</violation>
<violation number="2" location="src/lib/models/providers/openai/openaiLLM.ts:257">
P1: Raw model output is logged on JSON-repair failure, which can leak sensitive content into server logs.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| extractJson: true, | ||
| }) as string; | ||
| } catch (repairErr) { | ||
| console.error('repairJson failed on content:', content); |
There was a problem hiding this comment.
P1: Raw model output is logged on JSON-repair failure, which can leak sensitive content into server logs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/models/providers/openai/openaiLLM.ts, line 257:
<comment>Raw model output is logged on JSON-repair failure, which can leak sensitive content into server logs.</comment>
<file context>
@@ -229,13 +248,16 @@ class OpenAILLM extends BaseLLM<OpenAIConfig> {
+ extractJson: true,
+ }) as string;
+ } catch (repairErr) {
+ console.error('repairJson failed on content:', content);
+ throw new Error(`Failed to repair JSON: ${repairErr}`);
+ }
</file context>
| console.error('repairJson failed on content:', content); | |
| console.error('repairJson failed', { | |
| error: repairErr instanceof Error ? repairErr.message : String(repairErr), | |
| contentLength: content.length, | |
| }); |
| existingCall.arguments += tc.function?.arguments || ''; | ||
| return { | ||
| const argsToParse = existingCall.arguments || '{}'; | ||
| parsedToolCalls.push({ |
There was a problem hiding this comment.
P2: Parse failures in streaming tool calls are now swallowed and emitted as a synthetic {} arguments tool call, which downstream consumers execute without validation. This can trigger incorrect tool execution instead of surfacing the parse error.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/models/providers/openai/openaiLLM.ts, line 187:
<comment>Parse failures in streaming tool calls are now swallowed and emitted as a synthetic `{}` arguments tool call, which downstream consumers execute without validation. This can trigger incorrect tool execution instead of surfacing the parse error.</comment>
<file context>
@@ -163,27 +166,43 @@ class OpenAILLM extends BaseLLM<OpenAIConfig> {
existingCall.arguments += tc.function?.arguments || '';
- return {
+ const argsToParse = existingCall.arguments || '{}';
+ parsedToolCalls.push({
...existingCall,
- arguments: parse(existingCall.arguments),
</file context>
Summary
This PR fixes OpenRouter (and other OpenAI-compatible providers like LiteLLM) compatibility by replacing OpenAI-exclusive APIs with standard endpoints:
generateObject: Useschat.completions.createwithresponse_format: { type: 'json_object' }instead ofchat.completions.parse(which returns 404 on OpenRouter)streamObject: Useschat.completions.createwith streaming instead ofresponses.stream(OpenAI Responses API is not supported by other providers)Also adds a shared
parseJsonutility for stripping markdown code fences that LLMs sometimes wrap around JSON responses, even whenjson_objectmode is set.Root Cause
The OpenAI SDK's
chat.completions.parse()method calls the/chat/completions/parseendpoint, andresponses.stream()uses the Responses API — both are OpenAI-exclusive and not implemented by OpenRouter or other compatible providers.Tradeoffs
The tradeoff is worthwhile since client-side validation with
input.schema.parse()is already in place, and most non-OpenAI models don't support structured outputs anyway.Related
Test Plan
repairJson+stripMarkdownFenceslike other providers)Summary by cubic
Switch to the standard chat completions API to restore compatibility with OpenRouter and other OpenAI‑compatible providers. Adds JSON cleaning and safer parsing, plus stronger guards to prevent crashes during streaming and search.
generateObject: usechat.completions.createwithresponse_format: { type: 'json_object' }; add a system prompt to enforce pure JSON; handle empty content before parsing.streamObject: stream viachat.completions.create; strip code fences; parse partial JSON incrementally, yielding{}until parseable.stripMarkdownFences/safeParseJson; apply in OpenAI andollamaproviders to clean JSON before parsing.convertToOpenAIMessages; parse streaming tool call arguments with try/catch and fall back to{}on errors.webSearchqueries; defaultchatHistoryto[]; ensure SearXNG returns arrays on empty responses.Written for commit 424158b. Summary will update on new commits.