Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/extension/src/agent/useAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface AdvancedConfig {
maxSteps?: number
systemInstruction?: string
experimentalLlmsTxt?: boolean
disableNamedToolChoice?: boolean
}

export interface ExtConfig extends LLMConfig, AdvancedConfig {
Expand Down Expand Up @@ -124,6 +125,7 @@ export function useAgent(): UseAgentResult {
maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
...llmConfig
}: ExtConfig) => {
await chrome.storage.local.set({ llmConfig })
Comment on lines 127 to 131
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

disableNamedToolChoice is part of LLMConfig, but it’s destructured out here and therefore not included in the llmConfig object persisted to storage. That duplicates config concerns (it then gets stored under advancedConfig) and means anything reading only llmConfig will miss the flag. Consider keeping disableNamedToolChoice within llmConfig and out of AdvancedConfig so it persists with the other LLM settings.

Copilot uses AI. Check for mistakes.
Expand All @@ -132,7 +134,12 @@ export function useAgent(): UseAgentResult {
} else {
await chrome.storage.local.remove('language')
}
const advancedConfig: AdvancedConfig = { maxSteps, systemInstruction, experimentalLlmsTxt }
const advancedConfig: AdvancedConfig = {
maxSteps,
systemInstruction,
experimentalLlmsTxt,
disableNamedToolChoice,
}
await chrome.storage.local.set({ advancedConfig })
setConfig({ ...llmConfig, ...advancedConfig, language })
},
Expand Down
10 changes: 10 additions & 0 deletions packages/extension/src/components/ConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
const [experimentalLlmsTxt, setExperimentalLlmsTxt] = useState(
config?.experimentalLlmsTxt ?? false
)
const [disableNamedToolChoice, setDisableNamedToolChoice] = useState(
config?.disableNamedToolChoice ?? false
)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [saving, setSaving] = useState(false)
const [userAuthToken, setUserAuthToken] = useState<string>('')
Expand All @@ -51,6 +54,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
setMaxSteps(config?.maxSteps)
setSystemInstruction(config?.systemInstruction ?? '')
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
}, [config])

// Poll for user auth token every second until found
Expand Down Expand Up @@ -96,6 +100,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
maxSteps: maxSteps || undefined,
systemInstruction: systemInstruction || undefined,
experimentalLlmsTxt,
disableNamedToolChoice,
})
} finally {
setSaving(false)
Expand Down Expand Up @@ -271,6 +276,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
/>
</div>

<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Disable named tool_choice</span>
<Switch checked={disableNamedToolChoice} onCheckedChange={setDisableNamedToolChoice} />
</label>

<label className="flex items-center justify-between cursor-pointer">
<span className="text-xs text-muted-foreground">Experimental llms.txt support</span>
<Switch checked={experimentalLlmsTxt} onCheckedChange={setExperimentalLlmsTxt} />
Expand Down
11 changes: 7 additions & 4 deletions packages/llms/src/OpenAIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@ export class OpenAIClient implements LLMClient {
const openaiTools = Object.entries(tools).map(([name, t]) => zodToOpenAITool(name, t))

// Build request body

let toolChoice: unknown = 'required'
if (options?.toolChoiceName && !this.config.disableNamedToolChoice) {
toolChoice = { type: 'function', function: { name: options.toolChoiceName } }
}

const requestBody: Record<string, unknown> = {
model: this.config.model,
temperature: this.config.temperature,
messages,
tools: openaiTools,
parallel_tool_calls: false,
// Require tool call: specific tool if provided, otherwise any tool
tool_choice: options?.toolChoiceName
? { type: 'function', function: { name: options.toolChoiceName } }
: 'required',
tool_choice: toolChoice,
}

modelPatch(requestBody)
Expand Down
1 change: 1 addition & 0 deletions packages/llms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
apiKey: config.apiKey || '',
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
}
}
Expand Down
6 changes: 6 additions & 0 deletions packages/llms/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ export interface LLMConfig {
temperature?: number
maxRetries?: number

/**
* remove the tool_choice field from the request.
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
Comment on lines +99 to +100
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

The JSDoc for disableNamedToolChoice is misleading: it says this option removes the tool_choice field, but the implementation keeps tool_choice and uses the string value "required" (and only disables the named/object form when toolChoiceName is provided). Please update the comment to accurately describe the behavior and when it applies.

Suggested change
* remove the tool_choice field from the request.
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
* Disable the named/object tool_choice form when forcing a specific tool.
* When used together with `toolChoiceName`, the client sends `tool_choice: "required"`
* instead of `{ type: "function", function: { name: toolChoiceName } }`, keeping the
* `tool_choice` field but avoiding the object form that some LLMs reject with
* errors like "Invalid tool_choice type: 'object'".

Copilot uses AI. Check for mistakes.
*/
disableNamedToolChoice?: boolean

/**
* Custom fetch function for LLM API requests.
* Use this to customize headers, credentials, proxy, etc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ const result = await agent.execute('Fill in the form with test data')`}
defaultValue: '3',
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
},
{
name: 'disableNamedToolChoice',
type: 'boolean',
defaultValue: 'false',
description: isZh
? '禁用命名 tool_choice,始终使用 "required" 字符串。适用于不支持 tool_choice 对象格式的 LLM 服务。'
: 'Disable named tool_choice, always use "required" string. For LLM services that don\'t support the object format of tool_choice.',
},
{
name: 'customFetch',
type: 'typeof fetch',
Expand Down
Loading