diff --git a/.roo/skills/local-ci-precheck/SKILL.md b/.roo/skills/local-ci-precheck/SKILL.md new file mode 100644 index 0000000000..7e3622f983 --- /dev/null +++ b/.roo/skills/local-ci-precheck/SKILL.md @@ -0,0 +1,304 @@ +--- +name: local-ci-precheck +description: Pre-push CI check skill that runs 7 local CI checks (invisible characters, translations, ESLint, TypeScript, knip, unit tests, webview visual) before git push. Prevents CI failures by catching errors locally in ~5 minutes instead of waiting for GitHub Actions. Use when about to git push in the Zoo Code project. +--- + +# Local CI Pre-check Skill + +## When to Use This Skill + +Use this skill when: + +- Code mode or Light-Code mode is about to `git push` in the Zoo Code project +- You want to verify that all locally-runnable CI checks pass before pushing +- You want to catch lint errors, type errors, test failures, and dead code before CI + +## When NOT to Use This Skill + +Do NOT use this skill when: + +- The user explicitly passes `--skip-ci-check` +- Only non-source files changed (e.g., only `.md`, `.json` config files, `.yml` workflow files with no logic changes) +- Pushing to a branch that does not have CI enabled + +## Pre-conditions + +Before running checks, verify: + +1. Node.js is installed (`node --version`) +2. Dependencies are installed (`corepack pnpm install`) +3. Working directory is the Zoo Code project root + +## Checks (Sequential, Fastest-First Order) + +Run all 7 checks in order. **Stop at the first failure** and report. Each check includes Windows (PowerShell) and Linux/Mac (bash) commands. + +--- + +### Check 1: Invisible Characters (~2s) + +Detect zero-width characters, directional overrides, BOM, and soft hyphens that can cause subtle bugs. + +**Windows (PowerShell):** +```powershell +$patterns = '[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}\x{FEFF}\x{00AD}]' +Get-ChildItem -Recurse -Include *.ts,*.tsx,*.js,*.mjs,*.cjs,*.cts,*.mts,*.sh,*.yml,*.yaml -Exclude node_modules,dist,out,coverage,.turbo,.vinxi -Path src,webview-ui,packages,apps,.github | + Select-String -Pattern $patterns | + ForEach-Object { Write-Host "FOUND: $($_.Filename):$($_.LineNumber): $($_.Line)" } +``` + +**Linux/Mac (bash):** +```bash +grep -rnP '[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}\x{FEFF}\x{00AD}]' \ + --include='*.ts' --include='*.tsx' --include='*.js' --include='*.mjs' \ + --include='*.cjs' --include='*.cts' --include='*.mts' --include='*.sh' \ + --include='*.yml' --include='*.yaml' \ + --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=out \ + --exclude-dir=coverage --exclude-dir=.turbo --exclude-dir=.vinxi \ + src webview-ui packages apps .github +``` + +**Pass criteria:** No output (exit code 0). + +**Failure diagnosis:** +- If output appears, a file contains invisible Unicode characters +- The output shows `filename:line number: offending line` +- Open the file and remove the invisible character(s) +- Common culprits: copy-pasted text from web pages, accidental BOM from editors +- After removal, re-run Check 1 to confirm clean + +--- + +### Check 2: Check Translations (~5s) + +Verify all locale translation files are complete and no keys are missing. + +**Windows (PowerShell):** +```powershell +node scripts/find-missing-translations.js +``` + +**Linux/Mac (bash):** +```bash +node scripts/find-missing-translations.js +``` + +**Pass criteria:** Exit code 0, no "missing" output. + +**Failure diagnosis:** +- The script lists missing translation keys per locale +- Add the missing keys to each locale file under `src/i18n/locales/` and `webview-ui/src/i18n/locales/` +- Reference the English (`en`) file as the source of truth +- Use the `roo-translation` skill for translation guidelines +- After adding keys, re-run Check 2 to confirm + +--- + +### Check 3: Lint ESLint (~30s) + +Run ESLint with zero-warning tolerance and auto-prune stale suppressions. + +**Windows (PowerShell):** +```powershell +cd src; npx eslint --max-warnings=0 --prune-suppressions . +``` + +**Linux/Mac (bash):** +```bash +cd src && npx eslint --max-warnings=0 --prune-suppressions . +``` + +**Pass criteria:** Exit code 0, no warnings or errors. + +**Failure diagnosis:** + +*Scenario A: "There are suppressions left that do not occur anymore"* +- The `eslint-suppressions.json` file has stale entries from rules that were fixed +- The `--prune-suppressions` flag auto-removes them on successful run +- If the prune itself fails, manually open `src/eslint-suppressions.json` and remove entries for rules/files that no longer produce warnings +- After cleanup: `git add src/eslint-suppressions.json` and commit the change + +*Scenario B: ESLint rule violations* +- The output shows `filepath:line:col: error [rule-name] message` +- Open each file and fix the code according to the rule +- Run `npx eslint --max-warnings=0 --prune-suppressions .` again after each fix +- Common rules: `@typescript-eslint/no-unused-vars`, `no-console`, `prefer-const` + +--- + +### Check 4: Check Types (~60s) + +Run TypeScript type checking across all three project areas. + +**Windows (PowerShell):** +```powershell +cd src; npx tsc --noEmit +cd ..\webview-ui; npx tsc --noEmit +cd ..\packages\core; npx tsc --noEmit +``` + +**Linux/Mac (bash):** +```bash +cd src && npx tsc --noEmit +cd ../webview-ui && npx tsc --noEmit +cd ../packages/core && npx tsc --noEmit +``` + +**Pass criteria:** Exit code 0 for all three directories, zero type errors. + +**Failure diagnosis:** +- The output shows `filepath(line,col): error TSxxxx: message` +- `TS2322`: Type mismatch — check the expected vs actual type +- `TS2339`: Property does not exist — check the type definition or add the property +- `TS2345`: Argument type mismatch — cast or adjust the argument +- `TS2531`: Object is possibly null — add null check +- After fixing, re-run the failing directory's `tsc --noEmit` to confirm +- If a new type is introduced, ensure it is exported from the correct module + +--- + +### Check 5: Knip (~30s) + +Detect unused code, unused dependencies, and unlisted dependencies. + +**Windows (PowerShell):** +```powershell +corepack pnpm knip +``` + +**Linux/Mac (bash):** +```bash +corepack pnpm knip +``` + +**Pass criteria:** Exit code 0, no unused exports or unlisted dependencies reported. + +**Failure diagnosis:** +- **Unused exports**: Remove the unused function/variable/type, or prefix with `_` if intentionally unused +- **Unused dependencies**: Remove from `package.json` with `corepack pnpm remove ` +- **Unlisted dependencies**: Add the missing package to the correct `package.json` +- **Unused files**: Verify the file is truly unused, then delete it +- After fixes, re-run `corepack pnpm knip` to confirm + +--- + +### Check 6: Unit Tests (~120s) + +Run all unit and integration tests with coverage. + +**Windows (PowerShell):** +```powershell +corepack pnpm turbo run test:coverage +``` + +**Linux/Mac (bash):** +```bash +corepack pnpm turbo run test:coverage +``` + +**Alternative (run packages individually):** +```powershell +# Non-core packages +corepack pnpm turbo run test:coverage --filter="!@roo-code/core" + +# Core unit tests +corepack pnpm turbo run test:coverage:unit --filter="@roo-code/core" + +# Core integration tests +corepack pnpm turbo run test:coverage:integration --filter="@roo-code/core" +``` + +**Pass criteria:** Exit code 0, all tests pass, no coverage regression below threshold. + +**Failure diagnosis:** +- The output shows which test file and test case failed +- **Assertion failure**: Check the expected vs actual value in the test +- **Timeout**: The test may need more time or a mock may be missing +- **Import error**: A module may have been moved or renamed — update the import path +- Fix the failing test or the production code it tests +- Re-run only the failing package first: `cd && npx vitest run` to iterate faster +- Once individual package passes, re-run full suite: `corepack pnpm turbo run test:coverage` + +--- + +### Check 7: Webview Visual (~60s) + +Run webview UI snapshot tests to catch visual regressions. + +**Windows (PowerShell):** +```powershell +cd webview-ui; npx vitest run +``` + +**Linux/Mac (bash):** +```bash +cd webview-ui && npx vitest run +``` + +**Pass criteria:** Exit code 0, all snapshot tests pass. + +**Failure diagnosis:** +- **Snapshot mismatch**: If the visual change is intentional, update the snapshot: + ```bash + cd webview-ui && npx vitest run --update + ``` + Then review the diff in `webview-ui/src/__snapshots__/` and commit the updated snapshots +- **Unexpected layout shift**: Check CSS changes in webview-ui components +- **Missing snapshot baseline**: Run with `--update` to create initial snapshots +- If the difference is only font rendering (pixel-level), it may be a platform difference — verify the change looks correct visually + +--- + +## Result Format + +After all checks complete, output a summary table: + +``` +## Local CI Pre-check Results + +| # | Check Name | Status | Duration | Error Details | +|---|--------------------|--------|----------|---------------| +| 1 | Invisible Chars | ✅ PASS | 1.2s | — | +| 2 | Check Translations | ✅ PASS | 3.1s | — | +| 3 | Lint ESLint | ✅ PASS | 22.4s | — | +| 4 | Check Types | ❌ FAIL | 45.2s | TS2322 in src/utils.ts:42 | +| 5 | Knip | ⏭️ SKIP | — | Skipped due to Check 4 failure | +| 6 | Unit Tests | ⏭️ SKIP | — | Skipped due to Check 4 failure | +| 7 | Webview Visual | ⏭️ SKIP | — | Skipped due to Check 4 failure | + +**Result: FAILED** — Fix Check 4 (Check Types) before pushing. +``` + +**Rules:** +- If all 7 checks PASS → output `✅ All checks passed. Safe to push.` +- If any check FAILS → stop immediately, skip remaining checks, output the failure table +- Include the exact error message (first 3 lines) in the Error Details column +- Include the suggested fix below the table + +--- + +## Skip Conditions + +Skip the entire pre-check if ANY of the following is true: + +1. **Flag**: User passed `--skip-ci-check` in the push command +2. **Non-source only**: `git diff --name-only HEAD` shows only files matching: + - `*.md` + - `*.json` (excluding `package.json` and `tsconfig.json`) + - `*.yml` / `*.yaml` (excluding workflow logic changes) + - `.github/` label/config changes + - `docs/` directory changes + - `.gitignore`, `.gitattributes` + +When skipping, output: `⏭️ CI pre-check skipped (no source code changes or --skip-ci-check flag).` + +--- + +## Windows Environment Notes + +1. **Use `corepack pnpm`** instead of bare `pnpm` to avoid PowerShell execution policy errors (`pnpm.ps1 cannot be loaded`) +2. **Use `Select-String`** instead of `grep` for pattern matching in PowerShell +3. **Use `;`** as command separator in PowerShell (not `&&`) +4. **Use `cd dir; command`** pattern — PowerShell `cd` does not chain with `&&` like bash +5. **Path separators**: Use `\` in PowerShell commands, `/` in bash commands +6. **Exit code checking**: PowerShell does not propagate exit codes the same way as bash — check `$LASTEXITCODE` after external commands if needed diff --git a/apps/vscode-e2e/fixtures/terminal-lifecycle.json b/apps/vscode-e2e/fixtures/terminal-lifecycle.json new file mode 100644 index 0000000000..16e5472f14 --- /dev/null +++ b/apps/vscode-e2e/fixtures/terminal-lifecycle.json @@ -0,0 +1,32 @@ +{ + "fixtures": [ + { + "match": { + "userMessage": "TERMINAL_LIFECYCLE_E2E" + }, + "response": { + "toolCalls": [ + { + "name": "execute_command", + "arguments": "{\"command\":\"echo lifecycle-first\"}", + "id": "call_terminal_lifecycle_001" + } + ] + } + }, + { + "match": { + "userMessage": "TERMINAL_LIFECYCLE_CANCEL_E2E" + }, + "response": { + "toolCalls": [ + { + "name": "execute_command", + "arguments": "{\"command\":\"sleep 30\"}", + "id": "call_terminal_lifecycle_cancel_001" + } + ] + } + } + ] +} diff --git a/apps/vscode-e2e/src/fixtures/terminal-lifecycle.ts b/apps/vscode-e2e/src/fixtures/terminal-lifecycle.ts new file mode 100644 index 0000000000..88e6fc1dca --- /dev/null +++ b/apps/vscode-e2e/src/fixtures/terminal-lifecycle.ts @@ -0,0 +1,56 @@ +import { LLMock } from "@copilotkit/aimock" + +import { toolResultContains } from "./tool-result" + +/** + * Terminal lifecycle fixtures. + * + * The first command (call_terminal_lifecycle_001, echoed "lifecycle-first") + * completed on a fresh terminal. We respond by issuing a SECOND execute_command. + * The terminal lifecycle manager should reuse the still-warm terminal and run the + * second command through the command queue rather than spawning a brand-new + * terminal. When the second command's result ("lifecycle-second") arrives we + * finish the task. + */ +export function addTerminalLifecycleFixtures(mock: InstanceType) { + // First command completed -> issue a second command on the (reused) terminal. + mock.addFixture({ + match: { + predicate: (req) => + toolResultContains(req, "call_terminal_lifecycle_001", ["lifecycle-first", "Exit code: 0"]), + }, + response: { + toolCalls: [ + { + name: "execute_command", + arguments: JSON.stringify({ command: "echo lifecycle-second" }), + id: "call_terminal_lifecycle_002", + }, + ], + }, + }) + + // Second command (run on the reused terminal) completed -> finish the task. + mock.addFixture({ + match: { + predicate: (req) => + toolResultContains(req, "call_terminal_lifecycle_002", ["lifecycle-second", "Exit code: 0"]), + }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ + result: "Two commands ran through the terminal lifecycle: creation, reuse, and queueing.", + }), + id: "call_terminal_lifecycle_003", + }, + ], + }, + }) + + // Cancellation flow: the long-running command (call_terminal_lifecycle_cancel_001) + // is intentionally left without a follow-up fixture. The test cancels the task + // before the command would ever complete, exercising terminal cancellation and + // disposal. No additional fixtures are needed here. +} diff --git a/apps/vscode-e2e/src/runTest.ts b/apps/vscode-e2e/src/runTest.ts index 482e73e945..1690445df5 100644 --- a/apps/vscode-e2e/src/runTest.ts +++ b/apps/vscode-e2e/src/runTest.ts @@ -1,194 +1,196 @@ -import * as path from "path" -import * as os from "os" -import * as fs from "fs/promises" -import { readFileSync } from "fs" - -import { runTests } from "@vscode/test-electron" -import { LLMock } from "@copilotkit/aimock" - -import { addApplyDiffResultFixtures } from "./fixtures/apply-diff" -import { addDeepSeekV4Fixtures } from "./fixtures/deepseek-v4" -import { addExecuteCommandResultFixtures } from "./fixtures/execute-command" -import { addFastExitShellRaceResultFixtures } from "./fixtures/fast-exit-shell-race" -import { addZeroChunkShellRaceResultFixtures } from "./fixtures/zero-chunk-shell-race" -import { addTerminalReuseShellRaceFixtures } from "./fixtures/terminal-reuse-shell-race" -import { addLongRuningSilentCommandFixtures } from "./fixtures/long-running-silent-command" -import { addColdShellInitFixtures } from "./fixtures/cold-shell-init" -import { addTerminalProfileResultFixtures } from "./fixtures/terminal-profile" -import { addListFilesResultFixtures } from "./fixtures/list-files" -import { addReadFileResultFixtures } from "./fixtures/read-file" -import { addSearchFilesResultFixtures } from "./fixtures/search-files" -import { addSubtaskFixtures } from "./fixtures/subtasks" -import { addUseMcpToolResultFixtures } from "./fixtures/use-mcp-tool" -import { addWriteToFileResultFixtures } from "./fixtures/write-to-file" - -function getCliFlagValue(flag: string) { - return process.argv.find((arg, index) => process.argv[index - 1] === flag) -} - -function isDeepSeekTargetedRun(testFile?: string, testGrep?: string) { - if (testFile?.toLowerCase().includes("deepseek-v4.test")) { - return true - } - - // DeepSeek grep runs may target the suite name, file stem, or individual model IDs. - return testGrep?.toLowerCase().includes("deepseek") ?? false -} - -function isBedrockTargetedRun(testFile?: string, testGrep?: string) { - if (testFile?.toLowerCase().includes("bedrock.test")) { - return true - } - - return testGrep?.toLowerCase().includes("bedrock") ?? false -} - -async function main() { - const isRecord = process.env.AIMOCK_RECORD === "true" - const testGrep = getCliFlagValue("--grep") || process.env.TEST_GREP - const testFile = getCliFlagValue("--file") || process.env.TEST_FILE - const isDeepSeekTest = isDeepSeekTargetedRun(testFile, testGrep) - const isGeminiTest = testFile?.toLowerCase().includes("gemini.test") ?? false - const isBedrockTest = isBedrockTargetedRun(testFile, testGrep) - - if (isRecord && isDeepSeekTest && !process.env.DEEPSEEK_API_KEY) { - throw new Error("AIMOCK_RECORD=true requires DEEPSEEK_API_KEY to record DeepSeek fixtures") - } - - if (isRecord && isGeminiTest && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) { - throw new Error("AIMOCK_RECORD=true requires GEMINI_API_KEY to record Gemini fixtures") - } - - if (isRecord && !isDeepSeekTest && !isGeminiTest && !process.env.OPENROUTER_API_KEY) { - throw new Error("AIMOCK_RECORD=true requires OPENROUTER_API_KEY to record fixtures") - } - - // Record mode always needs aimock running (to capture traffic). - // Replay mode starts aimock when no real API key is present or USE_MOCK is forced. - const hasRealApiKey = isDeepSeekTest - ? !!process.env.DEEPSEEK_API_KEY - : isBedrockTest - ? true // Bedrock test starts its own binary-event-stream mock server when no real token - : !!(process.env.OPENROUTER_API_KEY || process.env.ANTHROPIC_API_KEY) - const useMock = isRecord || !hasRealApiKey || process.env.USE_MOCK === "true" - - let mock: InstanceType | undefined - - // The folder containing the Extension Manifest package.json - // Passed to `--extensionDevelopmentPath` - const extensionDevelopmentPath = path.resolve(__dirname, "../../../src") - - // The path to the extension test script - // Passed to --extensionTestsPath - const extensionTestsPath = path.resolve(__dirname, "./suite/index") - - let testWorkspace: string | undefined - - try { - // Create a temporary workspace folder for tests before installing fixtures that - // need workspace-specific paths. - testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-workspace-")) - - if (useMock) { - const fixturesDir = path.resolve(__dirname, "../fixtures") - - mock = new LLMock({ - port: 0, // random free port - ...(isRecord && { - record: { - // OpenRouter is OpenAI-compatible; aimock proxies using the openai provider key. - // Use /api (not /api/v1) — aimock appends the request path (/v1/chat/completions) - // so including /v1 here would produce a doubled /v1/v1 upstream URL. - providers: { - openai: isDeepSeekTest ? "https://api.deepseek.com" : "https://openrouter.ai/api", - // aimock forwards the x-api-key header from the Anthropic SDK to the real API. - anthropic: "https://api.anthropic.com", - // aimock forwards the x-goog-api-key header from the Google AI SDK. - ...(isGeminiTest && { gemini: "https://generativelanguage.googleapis.com" }), - }, - fixturePath: fixturesDir, - }, - }), - }) - - mock.loadFixtureDir(fixturesDir) - - if (!isRecord) { - addApplyDiffResultFixtures(mock) - addExecuteCommandResultFixtures(mock) - addFastExitShellRaceResultFixtures(mock) - addZeroChunkShellRaceResultFixtures(mock) - addTerminalReuseShellRaceFixtures(mock) - addLongRuningSilentCommandFixtures(mock) - addColdShellInitFixtures(mock) - addTerminalProfileResultFixtures(mock) - addListFilesResultFixtures(mock) - addReadFileResultFixtures(mock) - addSearchFilesResultFixtures(mock) - addSubtaskFixtures(mock) - addUseMcpToolResultFixtures(mock) - addWriteToFileResultFixtures(mock) - addDeepSeekV4Fixtures(mock) - - // The modes test (switch_mode → ask) triggers a second API call whose last - // user message starts with directly — no - // wrapper. JSON fixtures use substring matching so a bare "" - // match would collide with all other requests. A regex anchored to the start - // uniquely identifies this post-switch turn. Scope this fixture to the - // OpenRouter default model so provider-specific suites (e.g. DeepSeek) - // cannot accidentally match it. - mock.addFixture({ - match: { model: "openai/gpt-4.1", userMessage: /^/ }, - response: { - toolCalls: [ - { - name: "attempt_completion", - arguments: JSON.stringify({ result: "Switched to ❓ Ask mode as requested." }), - id: "call_modes_post_switch_001", - }, - ], - }, - }) - } - - await mock.start() - } - // Get test filter from command line arguments or environment variable - // Usage examples: - // - npm run test:e2e -- --grep "write-to-file" - // - TEST_GREP="apply-diff" npm run test:e2e - // - TEST_FILE="task.test.js" npm run test:e2e - - // Pass test filters and mock URL as environment variables to the test runner - const extensionTestsEnv = { - ...process.env, - ...(testGrep && { TEST_GREP: testGrep }), - ...(testFile && { TEST_FILE: testFile }), - ...(mock && { AIMOCK_URL: mock.url }), - ...(mock && { E2E_MOCK_MODEL_LIST_FALLBACK: "true" }), - } - - // Download VS Code, unzip it and run the integration test - // Read VS Code version from package.json to keep in sync with @types/vscode - const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../package.json"), "utf-8")) - const vscodeVersion = process.env.VSCODE_VERSION || pkg.devDependencies["@types/vscode"] - - await runTests({ - extensionDevelopmentPath, - extensionTestsPath, - launchArgs: [testWorkspace], - extensionTestsEnv, - version: vscodeVersion, - }) - } catch (error) { - console.error("Failed to run tests", error) - process.exitCode = 1 - } finally { - if (testWorkspace) { - await fs.rm(testWorkspace, { recursive: true, force: true }) - } - await mock?.stop() - } -} - -main() +import * as path from "path" +import * as os from "os" +import * as fs from "fs/promises" +import { readFileSync } from "fs" + +import { runTests } from "@vscode/test-electron" +import { LLMock } from "@copilotkit/aimock" + +import { addApplyDiffResultFixtures } from "./fixtures/apply-diff" +import { addDeepSeekV4Fixtures } from "./fixtures/deepseek-v4" +import { addExecuteCommandResultFixtures } from "./fixtures/execute-command" +import { addFastExitShellRaceResultFixtures } from "./fixtures/fast-exit-shell-race" +import { addZeroChunkShellRaceResultFixtures } from "./fixtures/zero-chunk-shell-race" +import { addTerminalReuseShellRaceFixtures } from "./fixtures/terminal-reuse-shell-race" +import { addTerminalLifecycleFixtures } from "./fixtures/terminal-lifecycle" +import { addLongRuningSilentCommandFixtures } from "./fixtures/long-running-silent-command" +import { addColdShellInitFixtures } from "./fixtures/cold-shell-init" +import { addTerminalProfileResultFixtures } from "./fixtures/terminal-profile" +import { addListFilesResultFixtures } from "./fixtures/list-files" +import { addReadFileResultFixtures } from "./fixtures/read-file" +import { addSearchFilesResultFixtures } from "./fixtures/search-files" +import { addSubtaskFixtures } from "./fixtures/subtasks" +import { addUseMcpToolResultFixtures } from "./fixtures/use-mcp-tool" +import { addWriteToFileResultFixtures } from "./fixtures/write-to-file" + +function getCliFlagValue(flag: string) { + return process.argv.find((arg, index) => process.argv[index - 1] === flag) +} + +function isDeepSeekTargetedRun(testFile?: string, testGrep?: string) { + if (testFile?.toLowerCase().includes("deepseek-v4.test")) { + return true + } + + // DeepSeek grep runs may target the suite name, file stem, or individual model IDs. + return testGrep?.toLowerCase().includes("deepseek") ?? false +} + +function isBedrockTargetedRun(testFile?: string, testGrep?: string) { + if (testFile?.toLowerCase().includes("bedrock.test")) { + return true + } + + return testGrep?.toLowerCase().includes("bedrock") ?? false +} + +async function main() { + const isRecord = process.env.AIMOCK_RECORD === "true" + const testGrep = getCliFlagValue("--grep") || process.env.TEST_GREP + const testFile = getCliFlagValue("--file") || process.env.TEST_FILE + const isDeepSeekTest = isDeepSeekTargetedRun(testFile, testGrep) + const isGeminiTest = testFile?.toLowerCase().includes("gemini.test") ?? false + const isBedrockTest = isBedrockTargetedRun(testFile, testGrep) + + if (isRecord && isDeepSeekTest && !process.env.DEEPSEEK_API_KEY) { + throw new Error("AIMOCK_RECORD=true requires DEEPSEEK_API_KEY to record DeepSeek fixtures") + } + + if (isRecord && isGeminiTest && !process.env.GEMINI_API_KEY && !process.env.GOOGLE_API_KEY) { + throw new Error("AIMOCK_RECORD=true requires GEMINI_API_KEY to record Gemini fixtures") + } + + if (isRecord && !isDeepSeekTest && !isGeminiTest && !process.env.OPENROUTER_API_KEY) { + throw new Error("AIMOCK_RECORD=true requires OPENROUTER_API_KEY to record fixtures") + } + + // Record mode always needs aimock running (to capture traffic). + // Replay mode starts aimock when no real API key is present or USE_MOCK is forced. + const hasRealApiKey = isDeepSeekTest + ? !!process.env.DEEPSEEK_API_KEY + : isBedrockTest + ? true // Bedrock test starts its own binary-event-stream mock server when no real token + : !!(process.env.OPENROUTER_API_KEY || process.env.ANTHROPIC_API_KEY) + const useMock = isRecord || !hasRealApiKey || process.env.USE_MOCK === "true" + + let mock: InstanceType | undefined + + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, "../../../src") + + // The path to the extension test script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, "./suite/index") + + let testWorkspace: string | undefined + + try { + // Create a temporary workspace folder for tests before installing fixtures that + // need workspace-specific paths. + testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-workspace-")) + + if (useMock) { + const fixturesDir = path.resolve(__dirname, "../fixtures") + + mock = new LLMock({ + port: 0, // random free port + ...(isRecord && { + record: { + // OpenRouter is OpenAI-compatible; aimock proxies using the openai provider key. + // Use /api (not /api/v1) — aimock appends the request path (/v1/chat/completions) + // so including /v1 here would produce a doubled /v1/v1 upstream URL. + providers: { + openai: isDeepSeekTest ? "https://api.deepseek.com" : "https://openrouter.ai/api", + // aimock forwards the x-api-key header from the Anthropic SDK to the real API. + anthropic: "https://api.anthropic.com", + // aimock forwards the x-goog-api-key header from the Google AI SDK. + ...(isGeminiTest && { gemini: "https://generativelanguage.googleapis.com" }), + }, + fixturePath: fixturesDir, + }, + }), + }) + + mock.loadFixtureDir(fixturesDir) + + if (!isRecord) { + addApplyDiffResultFixtures(mock) + addExecuteCommandResultFixtures(mock) + addFastExitShellRaceResultFixtures(mock) + addZeroChunkShellRaceResultFixtures(mock) + addTerminalReuseShellRaceFixtures(mock) + addTerminalLifecycleFixtures(mock) + addLongRuningSilentCommandFixtures(mock) + addColdShellInitFixtures(mock) + addTerminalProfileResultFixtures(mock) + addListFilesResultFixtures(mock) + addReadFileResultFixtures(mock) + addSearchFilesResultFixtures(mock) + addSubtaskFixtures(mock) + addUseMcpToolResultFixtures(mock) + addWriteToFileResultFixtures(mock) + addDeepSeekV4Fixtures(mock) + + // The modes test (switch_mode → ask) triggers a second API call whose last + // user message starts with directly — no + // wrapper. JSON fixtures use substring matching so a bare "" + // match would collide with all other requests. A regex anchored to the start + // uniquely identifies this post-switch turn. Scope this fixture to the + // OpenRouter default model so provider-specific suites (e.g. DeepSeek) + // cannot accidentally match it. + mock.addFixture({ + match: { model: "openai/gpt-4.1", userMessage: /^/ }, + response: { + toolCalls: [ + { + name: "attempt_completion", + arguments: JSON.stringify({ result: "Switched to ❓ Ask mode as requested." }), + id: "call_modes_post_switch_001", + }, + ], + }, + }) + } + + await mock.start() + } + // Get test filter from command line arguments or environment variable + // Usage examples: + // - npm run test:e2e -- --grep "write-to-file" + // - TEST_GREP="apply-diff" npm run test:e2e + // - TEST_FILE="task.test.js" npm run test:e2e + + // Pass test filters and mock URL as environment variables to the test runner + const extensionTestsEnv = { + ...process.env, + ...(testGrep && { TEST_GREP: testGrep }), + ...(testFile && { TEST_FILE: testFile }), + ...(mock && { AIMOCK_URL: mock.url }), + ...(mock && { E2E_MOCK_MODEL_LIST_FALLBACK: "true" }), + } + + // Download VS Code, unzip it and run the integration test + // Read VS Code version from package.json to keep in sync with @types/vscode + const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../package.json"), "utf-8")) + const vscodeVersion = process.env.VSCODE_VERSION || pkg.devDependencies["@types/vscode"] + + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [testWorkspace], + extensionTestsEnv, + version: vscodeVersion, + }) + } catch (error) { + console.error("Failed to run tests", error) + process.exitCode = 1 + } finally { + if (testWorkspace) { + await fs.rm(testWorkspace, { recursive: true, force: true }) + } + await mock?.stop() + } +} + +main() diff --git a/apps/vscode-e2e/src/suite/terminal-lifecycle.test.ts b/apps/vscode-e2e/src/suite/terminal-lifecycle.test.ts new file mode 100644 index 0000000000..fb8c3ec9a9 --- /dev/null +++ b/apps/vscode-e2e/src/suite/terminal-lifecycle.test.ts @@ -0,0 +1,121 @@ +/** + * E2E suite: Terminal Lifecycle Management (Unified Shell Resolution). + * + * Covers the terminal lifecycle surface added in PR #1135: + * + * 1. Creation -> reuse -> command queue. A task runs two sequential + * execute_command calls. The first creates a terminal; the second must + * reuse that warm terminal and be scheduled through the command queue + * rather than spawning a brand-new terminal. + * 2. Cancellation -> disposal. A task starts a long-running command and is + * then cancelled. The task aborts (TaskAborted) and the terminal is + * disposed/cleaned up rather than being left busy forever. + * + * The lifecycle manager itself lives in src/integrations/terminal/. These tests + * exercise it through the public RooCodeAPI against the built extension bundle + * (aimock fixtures), the same way the other terminal e2e suites do. + */ +import * as assert from "assert" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { waitUntilAborted, waitUntilCompleted } from "./utils" +import { setDefaultSuiteTimeout } from "./test-utils" + +suite("Terminal lifecycle (creation, reuse, queue, cancellation)", function () { + if (process.platform !== "linux") { + return + } + + setDefaultSuiteTimeout(this) + + setup(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + teardown(async () => { + try { + await globalThis.api.cancelCurrentTask() + } catch { + // task may not be running + } + }) + + test("creates a terminal, reuses it, and queues a second command", async function () { + const api = globalThis.api + const messages: ClineMessage[] = [] + let errorOccurred: string | null = null + + const messageHandler = ({ message }: { message: ClineMessage }) => { + messages.push(message) + if (message.type === "say" && message.say === "error") { + errorOccurred = message.text || "Unknown error" + } + } + api.on(RooCodeEventName.Message, messageHandler) + + try { + await waitUntilCompleted({ + api, + start: () => + api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowExecute: true, + allowedCommands: ["*"], + terminalShellIntegrationDisabled: false, + }, + text: "TERMINAL_LIFECYCLE_E2E", + }), + timeout: 60_000, + }) + + assert.strictEqual(errorOccurred, null, `Error occurred: ${errorOccurred}`) + + // The second command ran on the reused terminal (fixture only matches on + // "lifecycle-second" + "Exit code: 0"), so reaching completion proves the + // creation -> reuse -> queue flow succeeded end to end. + const completionMessage = messages.find( + (message) => message.type === "say" && message.say === "completion_result", + ) + assert.ok( + completionMessage, + "Task should have completed both commands and reached attempt_completion", + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + } + }) + + test("cancels a running command and aborts the task (terminal disposed)", async function () { + const api = globalThis.api + + // Start a task whose fixture issues a long-running command (sleep 30) that + // never completes on its own. We cancel it before the command finishes. + const taskId = await api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowExecute: true, + allowedCommands: ["*"], + terminalShellIntegrationDisabled: false, + }, + text: "TERMINAL_LIFECYCLE_CANCEL_E2E", + }) + + // Give the task a moment to start the long-running command before cancelling. + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + await api.cancelCurrentTask() + + // Cancellation must surface as a TaskAborted event for this task. If the + // terminal lifecycle left the terminal busy/undisposed, the abort path would + // hang and this would time out instead. + await waitUntilAborted({ api, taskId, timeout: 30_000 }) + }) +}) diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index cd786a6529..77e7527b40 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -166,3 +166,75 @@ describe("getApiProtocol", () => { }) }) }) + +describe("openAiToolStrictMode", () => { + it("should be optional and absent by default", () => { + const result = providerSettingsSchemaDiscriminated.parse({ + apiProvider: "openai", + openAiModelId: "test-model", + }) + expect(result.apiProvider).toBe("openai") + if (result.apiProvider === "openai") { + expect(result.openAiToolStrictMode).toBeUndefined() + } + }) + + it("should accept true when provided", () => { + const result = providerSettingsSchemaDiscriminated.parse({ + apiProvider: "openai", + openAiModelId: "test-model", + openAiToolStrictMode: true, + }) + expect(result.apiProvider).toBe("openai") + if (result.apiProvider === "openai") { + expect(result.openAiToolStrictMode).toBe(true) + } + }) + + it("should accept false when provided", () => { + const result = providerSettingsSchemaDiscriminated.parse({ + apiProvider: "openai", + openAiModelId: "test-model", + openAiToolStrictMode: false, + }) + expect(result.apiProvider).toBe("openai") + if (result.apiProvider === "openai") { + expect(result.openAiToolStrictMode).toBe(false) + } + }) + + it("should not break existing profile deserialization when absent", () => { + const existingProfile = { + apiProvider: "openai" as const, + openAiBaseUrl: "https://api.example.com/v1", + openAiApiKey: "sk-test", + openAiModelId: "gpt-4", + openAiStreamingEnabled: true, + } + const result = providerSettingsSchemaDiscriminated.parse(existingProfile) + expect(result.apiProvider).toBe("openai") + if (result.apiProvider === "openai") { + expect(result.openAiModelId).toBe("gpt-4") + expect(result.openAiToolStrictMode).toBeUndefined() + } + }) + + it("should only exist on the openai (OpenAI Compatible) provider profile", () => { + const openAiResult = providerSettingsSchemaDiscriminated.parse({ + apiProvider: "openai", + openAiToolStrictMode: true, + }) + expect(openAiResult.apiProvider).toBe("openai") + if (openAiResult.apiProvider === "openai") { + expect(openAiResult.openAiToolStrictMode).toBe(true) + } + + // Anthropic provider should not have this field + const anthropicResult = providerSettingsSchemaDiscriminated.parse({ + apiProvider: "anthropic", + apiKey: "sk-test", + }) + expect(anthropicResult.apiProvider).toBe("anthropic") + expect((anthropicResult as Record).openAiToolStrictMode).toBeUndefined() + }) +}) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index e17cd5ddbc..8cd868f297 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -248,6 +248,7 @@ const openAiSchema = baseProviderSettingsSchema.extend({ openAiStreamingEnabled: z.boolean().optional(), openAiHostHeader: z.string().optional(), // Keep temporarily for backward compatibility during migration. openAiHeaders: z.record(z.string(), z.string()).optional(), + openAiToolStrictMode: z.boolean().optional(), // Profile-scoped strict function-tool schema toggle for OpenAI Compatible provider. Absent = false (backward compatible). }) const ollamaSchema = baseProviderSettingsSchema.extend({ diff --git a/progress.txt b/progress.txt deleted file mode 100644 index b3983826b3..0000000000 --- a/progress.txt +++ /dev/null @@ -1,59 +0,0 @@ -# Reapplication Progress — rc6 branch cleanup -# Updated: 2026-02-15 - -## Completed Batches - -### Batch 1 — Clean cherry-picks (PR #11473) -- 22 PRs merged cleanly -- Status: MERGED to main - -### Batch 2 — Minor conflicts (PR #11474) -- 9 PRs with minor conflicts resolved -- Status: MERGED to main - -### Batch 3 — Skills Infrastructure & Browser Use Removal (4 PRs) -- PR #11102: skill mode dropdown (44 conflicts resolved) -- PR #11157: improve Skills/Slash Commands UI (6 conflicts resolved) -- PR #11414: remove built-in skills mechanism (4 conflicts resolved) -- PR #11392: remove browser use entirely (5 conflicts resolved) -- Status: ON BRANCH reapply/batch-3-4-5-major-conflicts - -### Batch 4 — Provider Removals (2 PRs) -- PR #11253: remove URL context/Grounding checkboxes (4 conflicts resolved) -- PR #11297: remove 9 low-usage providers + retired UX (14 conflicts resolved) -- Status: ON BRANCH reapply/batch-3-4-5-major-conflicts - -### Batch 5 — Azure Foundry -- PR #11315 and #11374: EXCLUDED — depends on AI-SDK (@ai-sdk/azure, from "ai") -- These PRs are AI-SDK-entangled and cannot be cherry-picked to the pre-AI-SDK codebase -- Status: DEFERRED (AI-SDK dependent) - -## Post-cherry-pick Fixes Applied -1. Restored gemini.ts + vertex.ts to pre-AI-SDK state (cherry-picks brought AI-SDK versions) -2. Restored ai-sdk.spec.ts, gemini-handler.spec.ts, vertex.spec.ts to pre-AI-SDK versions -3. Fixed processUserContentMentions.ts ghost import (rooMessage.ts doesn't exist) -4. Added missing skills type exports to @roo-code/types (SkillMetadata, validateSkillName, etc.) -5. Added SkillsSettings import to SettingsView.tsx -6. Added Dialog/Select/Collapsible mocks to SettingsView test files -7. Fixed Task.ts type mismatches (replaced local types with Anthropic SDK types) -8. Added skills state to ExtensionStateContext - -## Deferred PRs (AI-SDK Entangled) -- #11379: delegation (AI-SDK) -- #11418: delegation (AI-SDK) -- #11422: delegation (AI-SDK) -- #11315: Azure Foundry provider (AI-SDK) -- #11374: Azure Foundry fix (AI-SDK) - -## Validation Results -- Backend tests: ALL PASSED (5224 tests) -- UI tests: ALL PASSED (1267 tests) -- Type checks: ALL PASSED (14/14 packages) -- AI-SDK contamination: CLEAN (0 matches) - -## Notes -- Pre-push hook fails on `roo-cline:bundle` because `generate-built-in-skills.ts` was removed - by PR #11414 but `package.json` still references it in `prebundle`. This is expected and - will be resolved when the PR is merged to main and the script reference is cleaned up. -- Push was done with `--no-verify` after independent verification of types, backend tests, - and UI tests all passed cleanly. diff --git a/src/api/providers/__tests__/base-provider.spec.ts b/src/api/providers/__tests__/base-provider.spec.ts index ced452f5a5..66109e7cf3 100644 --- a/src/api/providers/__tests__/base-provider.spec.ts +++ b/src/api/providers/__tests__/base-provider.spec.ts @@ -28,8 +28,8 @@ class TestProvider extends BaseProvider { } // Expose protected method for testing - public testConvertToolsForOpenAI(tools: any[] | undefined): any[] | undefined { - return this.convertToolsForOpenAI(tools) + public testConvertToolsForOpenAI(tools: any[] | undefined, strictMode: boolean = false): any[] | undefined { + return this.convertToolsForOpenAI(tools, strictMode) } } @@ -176,6 +176,16 @@ describe("BaseProvider", () => { expect(result.additionalProperties).toBe(false) expect(result.required).toEqual([]) }) + + it("should add empty properties and required arrays to zero-argument object schemas", () => { + const result = provider.testConvertToolSchemaForOpenAI({ type: "object" }) + + expect(result).toMatchObject({ + additionalProperties: false, + properties: {}, + required: [], + }) + }) }) describe("convertToolsForOpenAI", () => { @@ -184,100 +194,230 @@ describe("BaseProvider", () => { expect(result).toBeUndefined() }) - it("should set strict: true for non-MCP tools", () => { + it("should preserve non-function tools unchanged", () => { const tools = [ { - type: "function", - function: { - name: "read_file", - description: "Read a file", - parameters: { type: "object", properties: {} }, - }, + type: "other_type", + data: "some data", }, ] const result = provider.testConvertToolsForOpenAI(tools) - expect(result?.[0].function.strict).toBe(true) + expect(result?.[0]).toEqual(tools[0]) }) - it("should set strict: false for MCP tools (mcp-- prefix)", () => { - const tools = [ - { - type: "function", - function: { - name: "mcp--github--get_me", - description: "Get current user", - parameters: { type: "object", properties: {} }, + describe("strictMode = false (default)", () => { + it("should set strict: false for non-MCP tools", () => { + const tools = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { type: "object", properties: {} }, + }, }, - }, - ] + ] - const result = provider.testConvertToolsForOpenAI(tools) + const result = provider.testConvertToolsForOpenAI(tools) - expect(result?.[0].function.strict).toBe(false) - }) + expect(result?.[0].function.strict).toBe(false) + }) - it("should apply schema conversion to non-MCP tools", () => { - const tools = [ - { - type: "function", - function: { - name: "read_file", - description: "Read a file", - parameters: { - type: "object", - properties: { - path: { type: "string" }, + it("should preserve original best-effort schema for non-MCP tools (no hardening)", () => { + const tools = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { + type: "object", + properties: { + path: { type: "string" }, + encoding: { type: ["string", "null"] }, + }, + // Note: no required array, no additionalProperties }, }, }, - }, - ] + ] + + const result = provider.testConvertToolsForOpenAI(tools) + + // Schema should NOT be hardened when strict is false + expect(result?.[0].function.parameters.additionalProperties).toBeUndefined() + expect(result?.[0].function.parameters.required).toBeUndefined() + // Nullable type should be preserved as-is + expect(result?.[0].function.parameters.properties.encoding.type).toEqual(["string", "null"]) + }) + + it("should set strict: false for MCP tools", () => { + const tools = [ + { + type: "function", + function: { + name: "mcp--github--get_me", + description: "Get current user", + parameters: { type: "object", properties: {} }, + }, + }, + ] - const result = provider.testConvertToolsForOpenAI(tools) + const result = provider.testConvertToolsForOpenAI(tools) - expect(result?.[0].function.parameters.additionalProperties).toBe(false) - expect(result?.[0].function.parameters.required).toEqual(["path"]) - }) + expect(result?.[0].function.strict).toBe(false) + }) - it("should not apply schema conversion to MCP tools in base-provider", () => { - // Note: In base-provider, MCP tools are passed through unchanged - // The openai-native provider has its own handling for MCP tools - const tools = [ - { - type: "function", - function: { - name: "mcp--github--get_me", - description: "Get current user", - parameters: { - type: "object", - properties: { - token: { type: "string" }, + it("should preserve original schema for MCP tools (no hardening)", () => { + const tools = [ + { + type: "function", + function: { + name: "mcp--github--get_me", + description: "Get current user", + parameters: { + type: "object", + properties: { + token: { type: "string" }, + }, + required: ["token"], }, - required: ["token"], }, }, - }, - ] + ] - const result = provider.testConvertToolsForOpenAI(tools) + const result = provider.testConvertToolsForOpenAI(tools) - // MCP tools pass through original parameters in base-provider - expect(result?.[0].function.parameters.additionalProperties).toBeUndefined() + expect(result?.[0].function.parameters.additionalProperties).toBeUndefined() + expect(result?.[0].function.parameters.required).toEqual(["token"]) + }) }) - it("should preserve non-function tools unchanged", () => { - const tools = [ - { - type: "other_type", - data: "some data", - }, - ] + describe("strictMode = true", () => { + it("should set strict: true for non-MCP tools", () => { + const tools = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { type: "object", properties: {} }, + }, + }, + ] - const result = provider.testConvertToolsForOpenAI(tools) + const result = provider.testConvertToolsForOpenAI(tools, true) - expect(result?.[0]).toEqual(tools[0]) + expect(result?.[0].function.strict).toBe(true) + }) + + it("should apply schema hardening to non-MCP tools", () => { + const tools = [ + { + type: "function", + function: { + name: "read_file", + description: "Read a file", + parameters: { + type: "object", + properties: { + path: { type: "string" }, + }, + }, + }, + }, + ] + + const result = provider.testConvertToolsForOpenAI(tools, true) + + expect(result?.[0].function.parameters.additionalProperties).toBe(false) + expect(result?.[0].function.parameters.required).toEqual(["path"]) + }) + + it("should harden nested objects and arrays in non-MCP tools", () => { + const tools = [ + { + type: "function", + function: { + name: "create_user", + description: "Create a user", + parameters: { + type: "object", + properties: { + user: { + type: "object", + properties: { + name: { type: "string" }, + }, + }, + tags: { + type: "array", + items: { + type: "object", + properties: { + label: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + ] + + const result = provider.testConvertToolsForOpenAI(tools, true) + + expect(result?.[0].function.parameters.additionalProperties).toBe(false) + expect(result?.[0].function.parameters.properties.user.additionalProperties).toBe(false) + expect(result?.[0].function.parameters.properties.tags.items.additionalProperties).toBe(false) + }) + + it("should ALWAYS set strict: false for MCP tools even when strictMode is true", () => { + const tools = [ + { + type: "function", + function: { + name: "mcp--github--get_me", + description: "Get current user", + parameters: { type: "object", properties: {} }, + }, + }, + ] + + const result = provider.testConvertToolsForOpenAI(tools, true) + + expect(result?.[0].function.strict).toBe(false) + }) + + it("should preserve original schema for MCP tools even when strictMode is true", () => { + const tools = [ + { + type: "function", + function: { + name: "mcp--github--get_me", + description: "Get current user", + parameters: { + type: "object", + properties: { + token: { type: "string" }, + optional_param: { type: ["string", "null"] }, + }, + required: ["token"], + }, + }, + }, + ] + + const result = provider.testConvertToolsForOpenAI(tools, true) + + // MCP schema should NOT be hardened + expect(result?.[0].function.parameters.additionalProperties).toBeUndefined() + expect(result?.[0].function.parameters.required).toEqual(["token"]) + // Nullable type preserved + expect(result?.[0].function.parameters.properties.optional_param.type).toEqual(["string", "null"]) + }) }) }) }) diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index e8146a999a..ef319811ed 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -885,7 +885,6 @@ describe("OpenAiHandler", () => { // No custom temperature set → `temperature` is omitted. tools: undefined, tool_choice: undefined, - parallel_tool_calls: true, }, { path: "/models/chat/completions" }, ) @@ -893,6 +892,7 @@ describe("OpenAiHandler", () => { // Verify max_tokens is NOT included when not explicitly set const callArgs = mockCreate.mock.calls[0][0] expect(callArgs).not.toHaveProperty("max_completion_tokens") + expect(callArgs).not.toHaveProperty("parallel_tool_calls") }) it("should handle non-streaming responses with Azure AI Inference Service", async () => { @@ -931,7 +931,6 @@ describe("OpenAiHandler", () => { ], tools: undefined, tool_choice: undefined, - parallel_tool_calls: true, }, { path: "/models/chat/completions" }, ) @@ -939,6 +938,7 @@ describe("OpenAiHandler", () => { // Verify max_tokens is NOT included when not explicitly set const callArgs = mockCreate.mock.calls[0][0] expect(callArgs).not.toHaveProperty("max_completion_tokens") + expect(callArgs).not.toHaveProperty("parallel_tool_calls") }) it("should handle completePrompt with Azure AI Inference Service", async () => { @@ -1014,6 +1014,7 @@ describe("OpenAiHandler", () => { it("should handle O3 model with streaming and include max_completion_tokens when includeMaxTokens is true", async () => { const o3Handler = new OpenAiHandler({ ...o3Options, + reasoningEffort: "high", includeMaxTokens: true, modelMaxTokens: 32000, modelTemperature: 0.5, @@ -1041,7 +1042,7 @@ describe("OpenAiHandler", () => { ], stream: true, stream_options: { include_usage: true }, - reasoning_effort: "medium", + reasoning_effort: "high", temperature: undefined, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 32000, @@ -1200,6 +1201,7 @@ describe("OpenAiHandler", () => { it("should handle O3 model non-streaming with reasoning_effort and max_completion_tokens when includeMaxTokens is true", async () => { const o3Handler = new OpenAiHandler({ ...o3Options, + reasoningEffort: "high", openAiStreamingEnabled: false, includeMaxTokens: true, modelTemperature: 0.3, @@ -1225,7 +1227,7 @@ describe("OpenAiHandler", () => { }, { role: "user", content: "Hello!" }, ], - reasoning_effort: "medium", + reasoning_effort: "high", temperature: undefined, // O3 models do not support deprecated max_tokens but do support max_completion_tokens max_completion_tokens: 65536, // Using default maxTokens from o3Options diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index f4928b0b0a..b9ddea3c8c 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -93,9 +93,14 @@ export abstract class BaseOpenAiCompatibleProvider messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], stream: true, stream_options: { include_usage: true }, - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, this.options.openAiToolStrictMode ?? false), tool_choice: metadata?.tool_choice, - parallel_tool_calls: metadata?.parallelToolCalls ?? true, + // Only send parallel_tool_calls when tools are present; some + // OpenAI-compatible providers (e.g. Upstage solar-open2) reject + // this field when no tools are supplied. + ...(metadata?.tools && metadata.tools.length > 0 + ? { parallel_tool_calls: metadata?.parallelToolCalls ?? true } + : {}), } // Add thinking parameter if reasoning is enabled and model supports it diff --git a/src/api/providers/base-provider.ts b/src/api/providers/base-provider.ts index 89366fb619..de25ad3c8f 100644 --- a/src/api/providers/base-provider.ts +++ b/src/api/providers/base-provider.ts @@ -23,11 +23,23 @@ export abstract class BaseProvider implements ApiHandler { abstract getModel(): { id: string; info: ModelInfo } /** - * Converts an array of tools to be compatible with OpenAI's strict mode. - * Filters for function tools, applies schema conversion to their parameters, - * and ensures all tools have consistent strict: true values. + * Converts an array of tools for OpenAI-compatible providers. + * Filters for function tools and applies schema conversion to their parameters. + * + * When `strictMode` is true, non-MCP function tools get `strict: true` and + * their schemas are hardened via `convertToolSchemaForOpenAI()` (adds + * `additionalProperties: false`, marks all properties required, etc.). + * + * When `strictMode` is false (default), non-MCP function tools get + * `strict: false` and their original best-effort schemas are preserved + * without hardening. This is semantically consistent: `strict: false` + * should not imply strict-schema transformations. + * + * MCP tools are ALWAYS `strict: false` with original parameters preserved, + * regardless of the `strictMode` setting, because MCP schemas may contain + * optional properties that must remain optional. */ - protected convertToolsForOpenAI(tools: any[] | undefined): any[] | undefined { + protected convertToolsForOpenAI(tools: any[] | undefined, strictMode: boolean = false): any[] | undefined { if (!tools) { return undefined } @@ -37,18 +49,40 @@ export abstract class BaseProvider implements ApiHandler { return tool } - // MCP tools use the 'mcp--' prefix - disable strict mode for them + // MCP tools use the 'mcp--' prefix - always disable strict mode // to preserve optional parameters from the MCP server schema const isMcp = isMcpTool(tool.function.name) + if (isMcp) { + return { + ...tool, + function: { + ...tool.function, + strict: false, + parameters: tool.function.parameters, + }, + } + } + + // Non-MCP function tools respect the strictMode setting + if (strictMode) { + return { + ...tool, + function: { + ...tool.function, + strict: true, + parameters: this.convertToolSchemaForOpenAI(tool.function.parameters), + }, + } + } + + // strictMode false: preserve original best-effort schema return { ...tool, function: { ...tool.function, - strict: !isMcp, - parameters: isMcp - ? tool.function.parameters - : this.convertToolSchemaForOpenAI(tool.function.parameters), + strict: false, + parameters: tool.function.parameters, }, } }) @@ -76,6 +110,11 @@ export abstract class BaseProvider implements ApiHandler { result.additionalProperties = false } + if (result.properties === undefined) { + result.properties = {} + result.required = [] + } + if (result.properties) { const allKeys = Object.keys(result.properties) // OpenAI strict mode requires ALL properties to be in required array diff --git a/src/api/providers/deepseek.ts b/src/api/providers/deepseek.ts index 2e85c016b0..023fc929fd 100644 --- a/src/api/providers/deepseek.ts +++ b/src/api/providers/deepseek.ts @@ -155,7 +155,7 @@ export class DeepSeekHandler extends OpenAiHandler { stream_options: { include_usage: true }, ...(thinking && { thinking }), ...(deepSeekReasoningEffort && { reasoning_effort: deepSeekReasoningEffort }), - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, this.options.openAiToolStrictMode ?? false), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } diff --git a/src/api/providers/friendli.ts b/src/api/providers/friendli.ts index a5507e355a..8507c58ba7 100644 --- a/src/api/providers/friendli.ts +++ b/src/api/providers/friendli.ts @@ -169,7 +169,7 @@ export class FriendliHandler extends BaseOpenAiCompatibleProvider 0 + ? { parallel_tool_calls: metadata?.parallelToolCalls ?? true } + : {}), } // Add max_tokens if needed @@ -231,9 +236,14 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ? convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]) : [systemMessage, ...convertToOpenAiMessages(messages)], // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, this.options.openAiToolStrictMode ?? false), tool_choice: metadata?.tool_choice, - parallel_tool_calls: metadata?.parallelToolCalls ?? true, + // Only send parallel_tool_calls when tools are present; some + // OpenAI-compatible providers (e.g. Upstage solar-open2) reject + // this field when no tools are supplied. + ...(metadata?.tools && metadata.tools.length > 0 + ? { parallel_tool_calls: metadata?.parallelToolCalls ?? true } + : {}), } // Add max_tokens if needed @@ -342,7 +352,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl messages: Anthropic.Messages.MessageParam[], metadata?: ApiHandlerCreateMessageMetadata, ): ApiStream { - const modelInfo = this.getModel().info + const { info: modelInfo, reasoning } = this.getModel() const methodIsAzureAiInference = this._isAzureAiInference(this.options.openAiBaseUrl) if (this.options.openAiStreamingEnabled ?? true) { @@ -359,10 +369,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ], stream: true, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), - reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, + ...(reasoning && reasoning), temperature: undefined, // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, this.options.openAiToolStrictMode ?? false), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } @@ -393,10 +403,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl }, ...convertToOpenAiMessages(messages), ], - reasoning_effort: modelInfo.reasoningEffort as "low" | "medium" | "high" | undefined, + ...(reasoning && reasoning), temperature: undefined, // Tools are always present (minimum ALWAYS_AVAILABLE_TOOLS) - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, this.options.openAiToolStrictMode ?? false), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, } diff --git a/src/api/providers/opencode-go.ts b/src/api/providers/opencode-go.ts index be53dc1c02..3e490ee5ce 100644 --- a/src/api/providers/opencode-go.ts +++ b/src/api/providers/opencode-go.ts @@ -189,7 +189,7 @@ export class OpencodeGoHandler extends RouterProvider implements SingleCompletio this.options.includeMaxTokens === true ? this.options.modelMaxTokens || maxTokens : maxTokens, stream: true, stream_options: { include_usage: true }, - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, this.options.openAiToolStrictMode ?? false), tool_choice: metadata?.tool_choice, parallel_tool_calls: metadata?.parallelToolCalls ?? true, ...(reasoningEffort && { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 3e59b4360b..d74920adff 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -327,7 +327,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH }, }), ...(reasoning && { reasoning }), - tools: this.convertToolsForOpenAI(metadata?.tools), + tools: this.convertToolsForOpenAI(metadata?.tools, this.options.openAiToolStrictMode ?? false), tool_choice: metadata?.tool_choice, } diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 8b11c128c7..e095954255 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -162,6 +162,16 @@ export const OpenAICompatible = ({ onChange={handleInputChange("openAiStreamingEnabled", noTransform)}> {t("settings:modelInfo.enableStreaming")} +
+ + {t("settings:modelInfo.strictToolSchemas")} + +
+ {t("settings:modelInfo.strictToolSchemasDescription")} +
+
{{serviceName}}. Si no esteu segur de quin model triar, Zoo Code funciona millor amb {{defaultModelId}}. També podeu cercar \"free\" per a opcions gratuïtes actualment disponibles.", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 5a8c05551f..0362034de3 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Kostenlos bis zu {{count}} Anfragen pro Minute. Danach hängt die Abrechnung von der Prompt-Größe ab.", "pricingDetails": "Weitere Informationen finden Sie unter Preisdetails.", "billingEstimate": "* Die Abrechnung ist eine Schätzung - die genauen Kosten hängen von der Prompt-Größe ab." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "Die Erweiterung ruft automatisch die neueste Liste der auf {{serviceName}} verfügbaren Modelle ab. Wenn du dir nicht sicher bist, welches Modell du wählen sollst, funktioniert Zoo Code am besten mit {{defaultModelId}}. Du kannst auch versuchen, nach \"kostenlos\" zu suchen, um die derzeit verfügbaren kostenlosen Optionen zu finden.", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 2aacc322f0..58c1c547d1 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1044,6 +1044,8 @@ "enableR1FormatTips": "Must be enabled when using R1 models such as QWQ to prevent 400 errors", "useAzure": "Use Azure", "azureApiVersion": "Set Azure API version", + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.", "gemini": { "freeRequests": "* Free up to {{count}} requests per minute. After that, billing depends on prompt size.", "pricingDetails": "For more info, see pricing details.", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 3f99fc1b14..bc18d9a263 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Gratis hasta {{count}} solicitudes por minuto. Después de eso, la facturación depende del tamaño del prompt.", "pricingDetails": "Para más información, consulte los detalles de precios.", "billingEstimate": "* La facturación es una estimación - el costo exacto depende del tamaño del prompt." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "La extensión obtiene automáticamente la lista más reciente de modelos disponibles en {{serviceName}}. Si no está seguro de qué modelo elegir, Zoo Code funciona mejor con {{defaultModelId}}. También puede buscar \"free\" para opciones sin costo actualmente disponibles.", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index ac0e6afb22..6f108fd669 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Gratuit jusqu'à {{count}} requêtes par minute. Après cela, la facturation dépend de la taille du prompt.", "pricingDetails": "Pour plus d'informations, voir les détails de tarification.", "billingEstimate": "* La facturation est une estimation - le coût exact dépend de la taille du prompt." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "L'extension récupère automatiquement la liste la plus récente des modèles disponibles sur {{serviceName}}. Si vous ne savez pas quel modèle choisir, Zoo Code fonctionne mieux avec {{defaultModelId}}. Vous pouvez également rechercher \"free\" pour les options gratuites actuellement disponibles.", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index b720a5db83..570c15c576 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* प्रति मिनट {{count}} अनुरोधों तक मुफ्त। उसके बाद, बिलिंग प्रॉम्प्ट आकार पर निर्भर करती है।", "pricingDetails": "अधिक जानकारी के लिए, मूल्य निर्धारण विवरण देखें।", "billingEstimate": "* बिलिंग एक अनुमान है - सटीक लागत प्रॉम्प्ट आकार पर निर्भर करती है।" - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "एक्सटेंशन {{serviceName}} पर उपलब्ध मॉडलों की नवीनतम सूची स्वचालित रूप से प्राप्त करता है। यदि आप अनिश्चित हैं कि कौन सा मॉडल चुनना है, तो Zoo Code {{defaultModelId}} के साथ सबसे अच्छा काम करता है। आप वर्तमान में उपलब्ध निःशुल्क विकल्पों के लिए \"free\" भी खोज सकते हैं।", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index c46cc5acf1..1caccdcc8a 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Gratis hingga {{count}} permintaan per menit. Setelah itu, penagihan tergantung pada ukuran prompt.", "pricingDetails": "Untuk info lebih lanjut, lihat detail harga.", "billingEstimate": "* Penagihan adalah estimasi - biaya sebenarnya tergantung pada ukuran prompt." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "Ekstensi secara otomatis mengambil daftar model terbaru yang tersedia di {{serviceName}}. Jika kamu tidak yakin model mana yang harus dipilih, Zoo Code bekerja terbaik dengan {{defaultModelId}}. Kamu juga dapat mencoba mencari \"free\" untuk opsi tanpa biaya yang saat ini tersedia.", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index ff00dacca7..488cfeb9dc 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Gratuito fino a {{count}} richieste al minuto. Dopo, la fatturazione dipende dalla dimensione del prompt.", "pricingDetails": "Per maggiori informazioni, vedi i dettagli sui prezzi.", "billingEstimate": "* La fatturazione è una stima - il costo esatto dipende dalle dimensioni del prompt." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "L'estensione recupera automaticamente l'elenco più recente dei modelli disponibili su {{serviceName}}. Se non sei sicuro di quale modello scegliere, Zoo Code funziona meglio con {{defaultModelId}}. Puoi anche cercare \"free\" per opzioni gratuite attualmente disponibili.", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index cdcb377cc9..b35c9d2b63 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* 1分間あたり{{count}}リクエストまで無料。それ以降は、プロンプトサイズに応じて課金されます。", "pricingDetails": "詳細は価格情報をご覧ください。", "billingEstimate": "* 課金は見積もりです - 正確な費用はプロンプトのサイズによって異なります。" - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "拡張機能は{{serviceName}}で利用可能な最新のモデルリストを自動的に取得します。どのモデルを選ぶべきか迷っている場合、Zoo Codeは{{defaultModelId}}で最適に動作します。また、「free」で検索すると、現在利用可能な無料オプションを見つけることができます。", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 4a7845ac2a..bad684704f 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* 분당 {{count}}개의 요청까지 무료. 이후에는 프롬프트 크기에 따라 요금이 부과됩니다.", "pricingDetails": "자세한 내용은 가격 정보를 참조하세요.", "billingEstimate": "* 요금은 추정치입니다 - 정확한 비용은 프롬프트 크기에 따라 달라집니다." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "확장 프로그램은 {{serviceName}}에서 사용 가능한 최신 모델 목록을 자동으로 가져옵니다. 어떤 모델을 선택해야 할지 확실하지 않다면, Zoo Code는 {{defaultModelId}}로 가장 잘 작동합니다. 현재 사용 가능한 무료 옵션을 찾으려면 \"free\"를 검색해 볼 수도 있습니다.", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 768018c3ef..b5a01ff69e 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Gratis tot {{count}} verzoeken per minuut. Daarna is de prijs afhankelijk van de promptgrootte.", "pricingDetails": "Zie prijsdetails voor meer info.", "billingEstimate": "* Facturering is een schatting - de exacte kosten hangen af van de promptgrootte." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "De extensie haalt automatisch de nieuwste lijst met modellen op van {{serviceName}}. Weet je niet welk model je moet kiezen? Zoo Code werkt het beste met {{defaultModelId}}. Je kunt ook zoeken op 'free' voor gratis opties die nu beschikbaar zijn.", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 37b37df875..cce57930be 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Darmowe do {{count}} zapytań na minutę. Po tym, rozliczanie zależy od rozmiaru podpowiedzi.", "pricingDetails": "Więcej informacji znajdziesz w szczegółach cennika.", "billingEstimate": "* Rozliczenie jest szacunkowe - dokładny koszt zależy od rozmiaru podpowiedzi." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "Rozszerzenie automatycznie pobiera najnowszą listę modeli dostępnych w {{serviceName}}. Jeśli nie jesteś pewien, który model wybrać, Zoo Code działa najlepiej z {{defaultModelId}}. Możesz również wyszukać \"free\", aby znaleźć obecnie dostępne opcje bezpłatne.", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index c3b91d6b58..7c86902d6c 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Gratuito até {{count}} requisições por minuto. Depois disso, a cobrança depende do tamanho do prompt.", "pricingDetails": "Para mais informações, consulte os detalhes de preços.", "billingEstimate": "* A cobrança é uma estimativa - o custo exato depende do tamanho do prompt." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "A extensão busca automaticamente a lista mais recente de modelos disponíveis em {{serviceName}}. Se você não tem certeza sobre qual modelo escolher, o Zoo Code funciona melhor com {{defaultModelId}}. Você também pode pesquisar por \"free\" para encontrar opções gratuitas atualmente disponíveis.", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index c428b31ec1..ae726e7ad1 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Бесплатно до {{count}} запросов в минуту. Далее тарификация зависит от размера подсказки.", "pricingDetails": "Подробнее о ценах.", "billingEstimate": "* Счёт — приблизительный, точная стоимость зависит от размера подсказки." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "Расширение автоматически получает актуальный список моделей на {{serviceName}}. Если не уверены, что выбрать, Zoo Code лучше всего работает с {{defaultModelId}}. Также попробуйте поискать \"free\" для бесплатных вариантов.", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index bccb1c08aa..8d67e8df06 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Dakikada {{count}} isteğe kadar ücretsiz. Bundan sonra, ücretlendirme istem boyutuna bağlıdır.", "pricingDetails": "Daha fazla bilgi için fiyatlandırma ayrıntılarına bakın.", "billingEstimate": "* Ücretlendirme bir tahmindir - kesin maliyet istem boyutuna bağlıdır." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "Uzantı {{serviceName}} üzerinde bulunan mevcut modellerin en güncel listesini otomatik olarak alır. Hangi modeli seçeceğinizden emin değilseniz, Zoo Code {{defaultModelId}} ile en iyi şekilde çalışır. Şu anda mevcut olan ücretsiz seçenekleri bulmak için \"free\" araması da yapabilirsiniz.", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 6b20b9a9a9..1677396fbd 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* Miễn phí đến {{count}} yêu cầu mỗi phút. Sau đó, thanh toán phụ thuộc vào kích thước lời nhắc.", "pricingDetails": "Để biết thêm thông tin, xem chi tiết giá.", "billingEstimate": "* Thanh toán là ước tính - chi phí chính xác phụ thuộc vào kích thước lời nhắc." - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "Tiện ích mở rộng tự động lấy danh sách mới nhất các mô hình có sẵn trên {{serviceName}}. Nếu bạn không chắc chắn nên chọn mô hình nào, Zoo Code hoạt động tốt nhất với {{defaultModelId}}. Bạn cũng có thể thử tìm kiếm \"free\" cho các tùy chọn miễn phí hiện có.", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index c206c26108..137b26e602 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -968,7 +968,9 @@ "freeRequests": "* 每分钟免费 {{count}} 个请求。之后,计费取决于提示大小。", "pricingDetails": "有关更多信息,请参阅定价详情。", "billingEstimate": "* 计费为估计值 - 具体费用取决于提示大小。" - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "自动获取 {{serviceName}} 上可用的最新模型列表。如果您不确定选择哪个模型,Zoo Code 与 {{defaultModelId}} 配合最佳。您还可以搜索\"free\"以查找当前可用的免费选项。", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 64eb5e0b29..84e35da06f 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -995,7 +995,9 @@ "freeRequests": "* 每分鐘可免費使用 {{count}} 次請求,超過後將依提示詞大小計費。", "pricingDetails": "詳細資訊請參閱定價說明。", "billingEstimate": "* 費用為估算值 - 實際費用取決於提示大小。" - } + }, + "strictToolSchemas": "Strict tool schemas", + "strictToolSchemasDescription": "Enables strict mode for function tool schemas, ensuring tool outputs match the schema exactly. Some providers may not support strict mode. MCP tools are always kept non-strict regardless of this setting. (This setting is saved per profile and also applies to other providers that use the OpenAI protocol within the same profile.)" }, "modelPicker": { "automaticFetch": "此擴充功能會自動從 {{serviceName}} 取得最新的可用模型清單。如果不確定要選哪個模型,建議使用 {{defaultModelId}},這是與 Zoo Code 最佳搭配的模型。您也可以搜尋「free」來檢視目前可用的免費選項。",