Skip to content

fix: respect max_completion_tokens and guard missing model capabilities - #275

Open
laizhengbao wants to merge 2 commits into
ericc-ch:masterfrom
laizhengbao:master
Open

laizhengbao wants to merge 2 commits into
ericc-ch:masterfrom
laizhengbao:master

Conversation

@laizhengbao

Copy link
Copy Markdown

fix: respect max_completion_tokens and guard missing model capabilities

Summary

Two independent bugs in the chat completions handler, both hit by
real-world clients (observed behind Cherry Studio / gpt-load):

  1. Upstream 400 when clients use max_completion_tokens.
    Modern OpenAI clients send the newer max_completion_tokens field
    instead of legacy max_tokens. handleCompletion unconditionally
    injects max_tokens from the model's advertised limits whenever
    max_tokens is unset, so upstream receives BOTH parameters and
    rejects the request:
    max_tokens and max_completion_tokens cannot both be set.

  2. 500 for models without capabilities.
    GitHub's /models endpoint lists some entries with NO
    capabilities object (observed: gpt-41-copilot,
    trajectory-compaction). Requesting one crashes the proxy:
    undefined is not an object (evaluating 'selectedModel?.capabilities.limits.max_output_tokens').

Changes

File Change
src/routes/chat-completions/handler.ts skip max_tokens injection when max_completion_tokens is present; optional-chain the capabilities lookup
src/services/copilot/create-chat-completions.ts add max_completion_tokens?: number | null to ChatCompletionsPayload
src/services/copilot/get-models.ts Model.capabilities made optional, matching the real API
src/lib/tokenizer.ts guard capabilities?.tokenizer lookup
tests/chat-completions.test.ts regression tests for both fixes

Relation to #220: that PR solves the same max_completion_tokens
interop by normalizing the parameter into max_tokens; this PR takes
the passthrough approach (the Copilot API accepts
max_completion_tokens natively, so forwarding it preserves client
intent). Happy to drop that half if #220 lands first — the
capabilities fix is orthogonal and not covered by any open PR.

Test plan

  • bun run typecheck / bun run lint / bun test all green
  • New regression tests:
    • payload carrying only max_completion_tokens is forwarded without
      an injected max_tokens
    • requesting a capability-less model no longer throws
  • Live-tested against a Copilot individual-plan endpoint:
    • max_completion_tokens-only request -> 200 (previously 500)
    • gpt-41-copilot without max_tokens -> clean upstream 4xx, no crash
    • plain gpt-4.1 without either parameter -> max_tokens: 16384
      still injected (existing behavior preserved)
    • streaming requests unaffected

Two fixes in the chat completions handler:

1. Clients that send the newer `max_completion_tokens` parameter
	 (instead of legacy `max_tokens`) receive a 400 from the Copilot API:
	 `max_tokens and max_completion_tokens cannot both be set`.
	 The handler injects its own `max_tokens` whenever the legacy field is
	 unset, and upstream rejects requests carrying both parameters.
	 Injection is now skipped when either parameter is present, and the
	 payload type gains an optional `max_completion_tokens` field so the
	 value is forwarded upstream as intended.

	 Related: ericc-ch#220 addresses the same interop by normalizing
	 `max_completion_tokens` into `max_tokens` instead; this commit takes
	 the passthrough approach. Happy to drop this half if ericc-ch#220 lands first.

2. GitHub's /models endpoint lists some entries WITHOUT a
	 `capabilities` object (observed: `gpt-41-copilot`,
	 `trajectory-compaction`). Requesting one crashed the proxy with a
	 500: `undefined is not an object (evaluating
	 'selectedModel?.capabilities.limits.max_output_tokens')`.
	 `Model.capabilities` is now optional to mirror the real API, and both
	 the handler and the tokenizer lookup guard against its absence.

Tested against a live Copilot endpoint (individual plan):
- `max_completion_tokens`-only request -> 200 (previously 500)
- capability-less model without max_tokens -> clean upstream error, no crash
- plain request without either parameter -> `max_tokens` still injected
- streaming requests unaffected
@coderabbitai

coderabbitai Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Walkthrough

The change adds model-session support and adapters for Responses and legacy Completions. It adds POST routes for both protocols and registers standard and /v1 endpoints. Responses-only models can use the Responses API through chat completion creation. The change also updates token-limit handling, tokenizer fallback behavior, and Copilot and API version values.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 19 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes both primary fixes: preserving max_completion_tokens and guarding missing model capabilities.
Description check ✅ Passed The description directly explains the two bugs, the implemented fixes, and the related regression tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/lib/api-config.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/lib/state.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

src/routes/completions/route.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

  • 14 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@laizhengbao
laizhengbao marked this pull request as ready for review September 27, 2026 09:33
Copilot AI lite review requested due to automatic review settings September 27, 2026 09:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

A moderate issue remains when max_completion_tokens is explicitly null, potentially forwarding both token-limit parameters.

Review effort: Lite
Findings: None

What changed in this PR

Updates chat completion handling for modern token limits and models with incomplete capability metadata.

Changes:

  • Supports max_completion_tokens without conflicting injection.
  • Makes capability and tokenizer lookups safe.
  • Adds regression tests.
File Summary
tests/​chat-completions.test.ts Adds regression coverage.
src/​services/​copilot/​get-models.ts Makes model capabilities optional.
src/​services/​copilot/​create-chat-completions.ts Adds max_completion_tokens support.
src/​routes/​chat-completions/​handler.ts Adjusts token-limit injection and capability access.
src/​lib/​tokenizer.ts Adds tokenizer fallback handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: c588bb46-3afe-46bc-89e9-85f7d04be650

📥 Commits

Reviewing files that changed from the base of the PR and between 6e3b966 and c2c2cdb.

📒 Files selected for processing (17)
  • src/lib/api-config.ts
  • src/lib/state.ts
  • src/routes/completions/route.ts
  • src/routes/responses/route.ts
  • src/server.ts
  • src/services/copilot/completions-adapter.ts
  • src/services/copilot/create-chat-completions.ts
  • src/services/copilot/create-model-session.ts
  • src/services/copilot/create-proxy-completions.ts
  • src/services/copilot/create-responses.ts
  • src/services/copilot/get-models.ts
  • src/services/copilot/responses-adapter.ts
  • src/services/github/get-copilot-usage.ts
  • tests/chat-completions.test.ts
  • tests/create-chat-completions.test.ts
  • tests/model-session.test.ts
  • tests/responses-adapter.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

📜 Review details
🔇 Additional comments (13)
tests/chat-completions.test.ts (1)

72-77: LGTM!

src/lib/api-config.ts (1)

14-14: 🎯 Functional Correctness

Do not flag 2026-01-09 based on the public REST version list.

githubHeaders is used only for GitHub's internal Copilot endpoints: /copilot_internal/v2/token and /copilot_internal/user. The public REST version documentation does not establish the version contract for these endpoints, and the supplied evidence does not show that they reject 2026-01-09. No version change is supported by this finding.

src/services/copilot/get-models.ts (1)

49-49: LGTM!

src/lib/state.ts (1)

1-1: LGTM!

Also applies to: 10-11

src/services/github/get-copilot-usage.ts (1)

41-41: LGTM!

src/services/copilot/create-model-session.ts (1)

82-88: LGTM!

tests/model-session.test.ts (1)

1-116: LGTM!

tests/create-chat-completions.test.ts (1)

12-18: LGTM!

src/services/copilot/create-chat-completions.ts (1)

7-15: LGTM!

Also applies to: 53-55, 61-61

tests/responses-adapter.test.ts (1)

1-202: LGTM!

src/routes/responses/route.ts (1)

1-68: LGTM!

src/routes/completions/route.ts (1)

1-66: LGTM!

src/server.ts (1)

6-6: LGTM!

Also applies to: 10-10, 22-22, 25-25, 31-31, 34-34

Comment on lines +22 to +23
const prompt =
Array.isArray(payload.prompt) ? payload.prompt.join("\n") : payload.prompt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve separate array prompts.

If a legacy request supplies prompt: ["first", "second"], this code sends one prompt containing "first\nsecond". The Completions contract treats array elements as separate prompts, so the route returns a completion for a different input and cannot return results for each prompt. Process each prompt separately and retain its result index, or reject array prompts rather than silently merging them. (github.com)

Comment on lines +25 to +28
const result: ChatCompletionsPayload = {
model: payload.model,
messages: [{ role: "user", content: prompt }],
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve n and every completion choice.

If a client requests n: 2, this conversion drops n, so the chat request uses its single-choice default. The non-streaming and streaming converters also select choices[0]; forwarding n alone would still discard additional choices. Transfer n, then convert every choice with its original index in both result paths. (github.com)

Comment on lines +44 to +57
return {
id: chat.id,
object: "text_completion",
created: chat.created,
model: chat.model,
choices: [
{
text: choice.message.content ?? "",
index: 0,
finish_reason: choice.finish_reason,
logprobs: null,
},
],
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Carry token usage into non-streaming completion results.

When the chat response contains usage, this conversion drops it. Legacy completion clients therefore cannot obtain the token counts from a successful chat-backed request. Add optional usage to ProxyCompletionResult and copy it here. (github.com)

Comment on lines +47 to +57
try {
const response = await fetch(`${copilotBaseUrl(state)}/models/session`, {
method: "POST",
headers: copilotHeaders(state),
body: JSON.stringify({ auto_mode: { model_hints: modelHints() } }),
})

if (!response.ok) {
consola.warn("Model session creation failed with status", response.status)
return undefined
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Cache session-creation failures and share one in-flight request.

createChatCompletions and createResponses await modelSessionHeaders on every request. When POST /models/session fails, for example because the account or SKU does not support it, ensureModelSession returns undefined and stores nothing. Every later chat request then makes one more upstream round trip before its real request. The fetch also has no timeout. A slow session endpoint therefore delays every chat request. Parallel first requests each start their own POST /models/session.

Store a short failure backoff. Share one pending promise. Add an abort timeout.

♻️ Proposed fix
+const FAILURE_BACKOFF_MS = 5 * 60 * 1000
+let retryAfter = 0
+let pending: Promise<ModelSession | undefined> | undefined
+
 export const ensureModelSession = async (): Promise<
   ModelSession | undefined
 > => {
   if (!state.copilotToken) return
 
   const current = state.modelSession
   if (current && current.expiresAt > Date.now()) return current
+  if (Date.now() < retryAfter) return undefined
+  pending ??= createSession().finally(() => {
+    pending = undefined
+  })
+  return pending
+}

In createSession, pass signal: AbortSignal.timeout(10_000) to fetch. On a non-OK status or an error, set retryAfter = Date.now() + FAILURE_BACKOFF_MS.

Comment on lines +25 to +27
const response = await fetch(
`${base}/v1/engines/${payload.model}/completions`,
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟡 Minor | ⚡ Quick win

Path Traversal

Reachability: External
Exploitability: Moderate
CWE: CWE-22 — Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Encode payload.model before inserting it into the proxy URL path.

payload.model comes from the request body, and the route checks only endsWith("-copilot"). A value such as ../../other/path?x=-copilot passes that check. URL parsing then normalizes the traversal and sends the Copilot bearer token to another path on the same proxy host. Encode the model as one path segment:

🔒️ Proposed fix
   const response = await fetch(
-    `${base}/v1/engines/${payload.model}/completions`,
+    `${base}/v1/engines/${encodeURIComponent(payload.model)}/completions`,

The impact depends on whether callers already have full use of the Copilot token.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(
`${base}/v1/engines/${payload.model}/completions`,
{
const response = await fetch(
`${base}/v1/engines/${encodeURIComponent(payload.model)}/completions`,
{

View in Security blast radius

Comment on lines +68 to +70
if (!isNullish(payload.tool_choice)) {
result.tool_choice = payload.tool_choice
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate the shape of an object-form tool_choice.

Chat Completions uses { type: "function", function: { name } }. The Responses API uses { type: "function", name }. Both conversion functions copy tool_choice unchanged. In both directions, a client that forces a specific tool sends the wrong shape upstream, and the request fails. The string values "none", "auto" and "required" are the same in both APIs.

🐛 Proposed fix
-    if (!isNullish(payload.tool_choice)) {
-      result.tool_choice = payload.tool_choice
-    }
+    const choice = payload.tool_choice
+    if (!isNullish(choice)) {
+      result.tool_choice =
+        typeof choice === "string" ? choice
+        : { type: "function", name: choice.function.name }
+    }
-    if (!isNullish(payload.tool_choice)) {
-      result.tool_choice =
-        payload.tool_choice as ChatCompletionsPayload["tool_choice"]
-    }
+    const choice = payload.tool_choice
+    if (typeof choice === "string") {
+      result.tool_choice = choice as ChatCompletionsPayload["tool_choice"]
+    } else if (
+      choice && typeof (choice as { name?: unknown }).name === "string"
+    ) {
+      result.tool_choice = {
+        type: "function",
+        function: { name: (choice as { name: string }).name },
+      }
+    }

Also applies to: 506-509

...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
},
logprobs: null,
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Map status: "incomplete" to finish_reason: "length".

A Responses result that stops at max_output_tokens has status: "incomplete". This code reports "stop" in that case. A chat client then treats a truncated reply as complete. completedEventToChunks also sets "stop" on the streaming path, so both paths need the same mapping.

🐛 Proposed fix
-        finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
+        finish_reason:
+          toolCalls.length > 0 ? "tool_calls"
+          : result.status === "incomplete" ? "length"
+          : "stop",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
finish_reason:
toolCalls.length > 0 ? "tool_calls"
: result.status === "incomplete" ? "length"
: "stop",

Comment on lines +519 to +532
if (item.type === "function_call") {
messages.push({
role: "assistant",
content: null,
tool_calls: [
{
id: item.call_id ?? "call_0",
type: "function",
function: { name: item.name ?? "", arguments: item.arguments ?? "" },
},
],
})
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Group consecutive function_call items into one assistant message.

Parallel tool calls reach this code as consecutive function_call items, followed by their function_call_output items. Each function_call becomes its own assistant message here. The result is assistant(call_1), assistant(call_2), tool(call_1), tool(call_2). Chat Completions requires the tool messages for all of an assistant message's tool_calls to come right after that message. The upstream rejects this sequence.

🐛 Proposed fix
   if (item.type === "function_call") {
-    messages.push({
-      role: "assistant",
-      content: null,
-      tool_calls: [
-        {
-          id: item.call_id ?? "call_0",
-          type: "function",
-          function: { name: item.name ?? "", arguments: item.arguments ?? "" },
-        },
-      ],
-    })
+    const call: ToolCall = {
+      id: item.call_id ?? `call_${messages.length}`,
+      type: "function",
+      function: { name: item.name ?? "", arguments: item.arguments ?? "" },
+    }
+    const last = messages.at(-1)
+    if (last?.role === "assistant") {
+      last.tool_calls = [...(last.tool_calls ?? []), call]
+    } else {
+      messages.push({ role: "assistant", content: null, tool_calls: [call] })
+    }
     return
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (item.type === "function_call") {
messages.push({
role: "assistant",
content: null,
tool_calls: [
{
id: item.call_id ?? "call_0",
type: "function",
function: { name: item.name ?? "", arguments: item.arguments ?? "" },
},
],
})
return
}
if (item.type === "function_call") {
const call: ToolCall = {
id: item.call_id ?? `call_${messages.length}`,
type: "function",
function: { name: item.name ?? "", arguments: item.arguments ?? "" },
}
const last = messages.at(-1)
if (last?.role === "assistant") {
last.tool_calls = [...(last.tool_calls ?? []), call]
} else {
messages.push({ role: "assistant", content: null, tool_calls: [call] })
}
return
}

Comment on lines +574 to +590
const responsesToolsToChatTools = (tools: unknown): Array<Tool> | undefined => {
if (!Array.isArray(tools)) return undefined
return tools.map((tool) => {
const record = tool as Record<string, unknown>
return {
type: "function",
function: {
name: String(record.name),
description:
typeof record.description === "string" ?
record.description
: undefined,
parameters: (record.parameters ?? {}) as Record<string, unknown>,
},
}
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Convert only type: "function" tools.

Responses clients can send built-in or custom tools, such as web_search or local_shell, that have no top-level name. responsesToolsToChatTools turns each of these into a function tool named "undefined", via String(record.name), with empty parameters. The upstream then rejects the request, or the model sees a false tool. Skip tools that are not functions.

🐛 Proposed fix
 const responsesToolsToChatTools = (tools: unknown): Array<Tool> | undefined => {
   if (!Array.isArray(tools)) return undefined
-  return tools.map((tool) => {
-    const record = tool as Record<string, unknown>
+  const converted = (tools as Array<Record<string, unknown>>)
+    .filter((record) => record.type === "function" && typeof record.name === "string")
+    .map((record) => {
     return {
       type: "function",
       function: {
-        name: String(record.name),
+        name: record.name as string,

After the map, return converted.length > 0 ? converted : undefined.

Comment on lines +666 to +680
const collectToolCalls = (
choice: ChatCompletionChunk["choices"][0],
toolCalls: Array<ToolCall>,
) => {
for (const call of choice.delta.tool_calls ?? []) {
toolCalls.push({
id: call.id ?? `call_${toolCalls.length}`,
type: "function",
function: {
name: call.function?.name ?? "",
arguments: call.function?.arguments ?? "",
},
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Merge streamed tool-call deltas by index.

Chat Completions streams one tool call across many chunks. The first delta holds id and function.name. Each later delta holds only an arguments fragment with the same index. collectToolCalls pushes a new ToolCall for every delta. response.completed therefore holds one function call with the name and empty arguments, plus many nameless calls that each hold one argument fragment. Streaming tool use from Responses clients breaks on chat-only models.

🐛 Proposed fix
 const collectToolCalls = (
   choice: ChatCompletionChunk["choices"][0],
   toolCalls: Array<ToolCall>,
 ) => {
   for (const call of choice.delta.tool_calls ?? []) {
-    toolCalls.push({
-      id: call.id ?? `call_${toolCalls.length}`,
+    const existing = toolCalls[call.index]
+    if (existing) {
+      if (call.id) existing.id = call.id
+      if (call.function?.name) existing.function.name += call.function.name
+      existing.function.arguments += call.function?.arguments ?? ""
+      continue
+    }
+    toolCalls[call.index] = {
+      id: call.id ?? `call_${call.index}`,
       type: "function",
       function: {
         name: call.function?.name ?? "",
         arguments: call.function?.arguments ?? "",
       },
-    })
+    }
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const collectToolCalls = (
choice: ChatCompletionChunk["choices"][0],
toolCalls: Array<ToolCall>,
) => {
for (const call of choice.delta.tool_calls ?? []) {
toolCalls.push({
id: call.id ?? `call_${toolCalls.length}`,
type: "function",
function: {
name: call.function?.name ?? "",
arguments: call.function?.arguments ?? "",
},
})
}
}
const collectToolCalls = (
choice: ChatCompletionChunk["choices"][0],
toolCalls: Array<ToolCall>,
) => {
for (const call of choice.delta.tool_calls ?? []) {
const existing = toolCalls[call.index]
if (existing) {
if (call.id) existing.id = call.id
if (call.function?.name) existing.function.name += call.function.name
existing.function.arguments += call.function?.arguments ?? ""
continue
}
toolCalls[call.index] = {
id: call.id ?? `call_${call.index}`,
type: "function",
function: {
name: call.function?.name ?? "",
arguments: call.function?.arguments ?? "",
},
}
}
}

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants