Compare commits

..
Author SHA1 Message Date
Saoud Rizwan 131e25e1a1 chore(sdk): release v0.0.64 2026-07-16 18:14:58 -07:00
Saoud Rizwan a7ff007af9 chore(cli): release v3.0.43 2026-07-16 17:52:46 -07:00
ef27f45080 fix: max output token handling (#12031)
* fix: max output token handling

* shared

* max reasoning budgetTokens

* fix unit test

* fix: address review feedback on max output token handling

- OpenRouter effort branch sends only reasoning.effort (OpenRouter rejects
  effort combined with reasoning.max_tokens)
- OpenAI Responses forwards explicit caller maxTokens for API-key usage;
  ChatGPT OAuth and synthesized gateway defaults are still omitted
- Gateway lifts the synthesized default output cap above explicit Anthropic
  reasoning budgets so max_tokens > thinking.budget_tokens holds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address second-round review feedback on max output token handling

- Replace the gateway-only requestedMaxTokens field with a defaultedMaxTokens
  flag set when the gateway synthesizes a cap, so explicit maxTokens from
  direct provider callers is forwarded by default (greptile P1)
- Check the parsed hostname instead of a URL substring when detecting the
  ChatGPT OAuth backend (CodeQL)
- Drop the empty else-if branch in toAiSdkMessages in favor of an explicit
  emptiedByDroppedReasoning condition (greptile P2; biome rejects the
  suggested bare continue)
- Dedupe isPositiveFiniteNumber by exporting it from gateway.ts (greptile P2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: extract isPositiveFiniteNumber into providers/utils.ts

Move the shared helper to its own module as suggested in review instead
of exporting it from gateway.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: remove unrelated VS Code changes

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
2026-07-16 17:50:22 -07:00
Dominic Cooney e3c6d51072 fix: recognize frontmatter with a leading UTF-8 BOM (#12277)
* fix(vscode): recognize SKILL.md frontmatter with a leading UTF-8 BOM

SKILL.md files saved with a UTF-8 BOM (e.g. by Windows Notepad's 'UTF-8 with BOM' encoding) were silently skipped and not recognized as skills, because gray-matter/regex-style frontmatter parsers require '---' at byte offset 0 and never accounted for the leading BOM byte sequence Node's utf-8 decoder does not strip.

Fixes the shared parseYamlFrontmatter() helper (used by skills, rules, workflows, and remote skill entries in the VS Code extension) and every duplicated ad-hoc frontmatter regex across the SDK/CLI/hub/desktop-app/example-plugin code paths to strip a leading BOM before matching.

Adds regression tests exercising the exact reported scenario (BOM-prefixed SKILL.md silently missing name/description) in frontmatter.test.ts, skills.test.ts, skill-frontmatter-toggle.test.ts, user-instruction-config-loader.test.ts, and configured-agent-config.test.ts.

Fixes https://github.com/cline/cline/issues/12151

* refactor(shared): centralize UTF-8 BOM stripping

* refactor(shared): add UTF-8 file readers

* docs: guide UTF-8 configuration reads
2026-07-17 09:37:20 +09:00
50 changed files with 842 additions and 127 deletions
+1
View File
@@ -16,6 +16,7 @@ This file is the secret sauce for working effectively in this codebase. It captu
- The whole repo (including `apps/vscode`) uses **bun** for package management and task running. Emit `bun run X` / `bun install` / `bunx <bin>` / `bun file.ts`, never npm/npx. Node remains the *runtime* (VS Code's extension host and the standalone cline-core are Node), so Node-runtime tokens are legitimate and must not be "fixed" to bun — see @.clinerules/bun-and-node.md for the keep-list vs rewrite-list.
- Avoid provider-specific string matching / hardcoded provider branches when fixing provider/config plumbing. Prefer provider metadata, shared catalog/defaults, explicit protocol/client capabilities, or centralized normalization utilities that apply by data shape rather than `providerId === "..."`. If a provider exception seems necessary, stop and explain why instead of adding ad-hoc string matching.
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `bun run compile`, not `bun run build`).
- When reading a configuration files that users may edit, use `readFileStrippingUtf8Bom`, `readFileSyncStrippingUtf8Bom`, or `stripUtf8Bom` from `@cline/shared/node`. DON'T strip byte order marks of user files handled by tools/passed to models.
- When creating PRs, contributors should not create changelog-entry files. Maintainers handle release versioning and changelog curation during the release process.
- When adding new feature flags, see this PR as a reference https://github.com/cline/cline/pull/7566
- Additional instructions about making requests: @.clinerules/network.md
+4
View File
@@ -1,5 +1,9 @@
# Cline CLI Changelog
## 3.0.43
- The CLI now automatically trusts your operating system's certificate store, so it works behind corporate proxies and TLS-inspecting firewalls without manually setting `NODE_EXTRA_CA_CERTS` (fixes "unable to get local issuer certificate" errors, including Windows intermediate CA stores)
## 3.0.42
- Fixed Ollama native API routing so context window and timeout settings work again
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/cli",
"displayName": "cline",
"version": "3.0.42",
"version": "3.0.43",
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
"type": "module",
"publishConfig": {
+3 -2
View File
@@ -1,4 +1,4 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { basename, extname, join } from "node:path";
import {
@@ -15,6 +15,7 @@ import {
type SkillConfig,
type WorkflowConfig,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import { Command } from "commander";
import { getToolCatalog } from "../runtime/tools";
import { loadInteractiveConfigData } from "../tui/interactive-config";
@@ -209,7 +210,7 @@ async function runAgentsConfigCommand(
continue;
}
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const frontmatter = frontmatterMatch?.[1] ?? "";
const nameMatch = frontmatter.match(/^\s*name:\s*(.+?)\s*$/m);
+2 -1
View File
@@ -27,6 +27,7 @@ import {
type UserInstructionConfigService,
type WorkflowConfig,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import { getToolCatalog } from "../runtime/tools";
import {
type InteractiveSlashCommand,
@@ -195,7 +196,7 @@ function loadAgentConfigItems(workspaceRoot: string): InteractiveConfigItem[] {
continue;
}
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const frontmatterMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const frontmatter = frontmatterMatch?.[1] ?? "";
const nameMatch = frontmatter.match(/^\s*name:\s*(.+?)\s*$/m);
@@ -18,6 +18,7 @@ import {
resolvePluginConfigSearchPaths,
resolveAgentConfigSearchPaths as resolveSharedAgentConfigSearchPaths,
} from "@cline/core";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import { readMcpServersResponse } from "./mcp";
import type { JsonRecord } from "./types";
@@ -114,7 +115,7 @@ export async function listUserInstructionConfigs(
const ext = extname(entry.name).toLowerCase();
if (ext !== ".yml" && ext !== ".yaml") continue;
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const fm = fmMatch?.[1] ?? "";
const nameMatch = fm.match(/^\s*name:\s*(.+?)\s*$/m);
@@ -50,6 +50,7 @@ import {
updateMcpSettingsFileSync,
} from "@cline/core";
import { getClineEnvironmentConfig } from "@cline/shared";
import { readFileSyncStrippingUtf8Bom } from "@cline/shared/node";
import {
connectorChannelsPayload,
startConnectorChannel,
@@ -558,7 +559,7 @@ async function listUserInstructionConfigs(
const ext = extname(entry.name).toLowerCase();
if (ext !== ".yml" && ext !== ".yaml") continue;
const filePath = join(directory, entry.name);
const raw = readFileSync(filePath, "utf8");
const raw = readFileSyncStrippingUtf8Bom(filePath);
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
const fm = fmMatch?.[1] ?? "";
const nameMatch = fm.match(/^\s*name:\s*(.+?)\s*$/m);
@@ -56,4 +56,16 @@ describe("parseYamlFrontmatter", () => {
expect(result.data).to.deep.equal({ count: 42, enabled: true, tags: ["a", "b"] })
expect(result.body.trim()).to.equal("Content")
})
// Regression test for https://github.com/cline/cline/issues/12151
// A leading UTF-8 BOM (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not
// prevent frontmatter from being recognized.
it("parses frontmatter correctly when the content has a leading UTF-8 BOM", () => {
const input = `\uFEFF---\nname: my-skill\ndescription: A test skill\n---\n# my-skill\nThis is a test skill.`
const result = parseYamlFrontmatter(input)
expect(result.hadFrontmatter).to.equal(true)
expect(result.parseError).to.equal(undefined)
expect(result.data).to.deep.equal({ name: "my-skill", description: "A test skill" })
expect(result.body.trim()).to.equal("# my-skill\nThis is a test skill.")
})
})
@@ -139,6 +139,34 @@ Instructions here`)
expect(skills[0].source).to.equal("global")
})
// Regression test for https://github.com/cline/cline/issues/12151:
// SKILL.md files saved with a UTF-8 BOM (e.g. by Windows Notepad's "UTF-8 with BOM"
// encoding) were silently skipped because the frontmatter regex required "---" at the
// very start of the file and never accounted for the leading \uFEFF byte sequence.
it("should discover skills whose SKILL.md starts with a UTF-8 BOM", async () => {
const skillDir = path.join(GLOBAL_SKILLS_DIR, "my-skill")
const skillMdPath = path.join(skillDir, "SKILL.md")
fileExistsStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
fileExistsStub.withArgs(skillMdPath).resolves(true)
isDirectoryStub.withArgs(GLOBAL_SKILLS_DIR).resolves(true)
readdirStub.withArgs(GLOBAL_SKILLS_DIR).resolves(["my-skill"])
statStub.withArgs(skillDir).resolves({ isDirectory: () => true })
readFileStub.withArgs(skillMdPath, "utf-8").resolves(`\uFEFF---
name: my-skill
description: A test skill
---
# my-skill
This is a test skill.`)
const skills = await discoverSkills(TEST_CWD)
expect(skills).to.have.lengthOf(1)
expect(skills[0].name).to.equal("my-skill")
expect(skills[0].description).to.equal("A test skill")
expect(skills[0].source).to.equal("global")
})
it("should discover skills from project .clinerules/skills directory", async () => {
const projectSkillsDir = path.join(TEST_CWD, ".clinerules", "skills")
const skillDir = path.join(projectSkillsDir, "explaining-code")
@@ -1,3 +1,4 @@
import { stripUtf8Bom } from "@cline/shared"
import * as yaml from "js-yaml"
export type FrontmatterParseResult = {
@@ -35,11 +36,16 @@ export type FrontmatterParseResult = {
* - If no frontmatter exists, returns data={} and body=original markdown.
*/
export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
const normalizedMarkdown = stripUtf8Bom(markdown)
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
const match = markdown.match(frontmatterRegex)
const match = normalizedMarkdown.match(frontmatterRegex)
if (!match) {
return { data: {}, body: markdown, hadFrontmatter: false }
return { data: {}, body: normalizedMarkdown, hadFrontmatter: false }
}
const [, yamlContent, body] = match
@@ -48,6 +54,6 @@ export function parseYamlFrontmatter(markdown: string): FrontmatterParseResult {
return { data, body, hadFrontmatter: true }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { data: {}, body: markdown, hadFrontmatter: true, parseError: message }
return { data: {}, body: normalizedMarkdown, hadFrontmatter: true, parseError: message }
}
}
+35 -45
View File
@@ -19,7 +19,7 @@
},
"apps/cli": {
"name": "@cline/cli",
"version": "3.0.42",
"version": "3.0.43",
"bin": {
"cline": "src/index.ts",
},
@@ -632,7 +632,7 @@
},
"sdk/packages/agents": {
"name": "@cline/agents",
"version": "0.0.63",
"version": "0.0.64",
"dependencies": {
"@cline/llms": "workspace:*",
"@cline/shared": "workspace:*",
@@ -641,7 +641,7 @@
},
"sdk/packages/core": {
"name": "@cline/core",
"version": "0.0.63",
"version": "0.0.64",
"dependencies": {
"@cline/agents": "workspace:*",
"@cline/llms": "workspace:*",
@@ -679,7 +679,7 @@
},
"sdk/packages/llms": {
"name": "@cline/llms",
"version": "0.0.63",
"version": "0.0.64",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^4.0.89",
"@ai-sdk/anthropic": "^3.0.68",
@@ -714,14 +714,14 @@
},
"sdk/packages/sdk": {
"name": "@cline/sdk",
"version": "0.0.63",
"version": "0.0.64",
"dependencies": {
"@cline/core": "workspace:*",
},
},
"sdk/packages/shared": {
"name": "@cline/shared",
"version": "0.0.63",
"version": "0.0.64",
"dependencies": {
"aws4fetch": "^1.0.20",
"jsonrepair": "^3.13.2",
@@ -1796,7 +1796,7 @@
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
"@puppeteer/browsers": ["@puppeteer/browsers@2.13.2", "", { "dependencies": { "debug": "^4.4.3", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.4", "tar-fs": "^3.1.1", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw=="],
"@puppeteer/browsers": ["@puppeteer/browsers@2.6.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
@@ -4610,7 +4610,7 @@
"rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
@@ -5734,8 +5734,6 @@
"@react-aria/menu/@react-stately/collections": ["@react-stately/collections@3.13.1", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-stately": "^3.46.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-o1QSrtHyR7ODTdPyna87pZlgzvxBFOR8nI8XB+tQLIW2AMhE76pLYu4TN9CrZVy6nSAtE06IntyBV4toJIhorA=="],
"@react-aria/selection/@react-types/shared": ["@react-types/shared@3.36.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ=="],
"@react-aria/table/@react-stately/collections": ["@react-stately/collections@3.13.1", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-stately": "^3.46.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-o1QSrtHyR7ODTdPyna87pZlgzvxBFOR8nI8XB+tQLIW2AMhE76pLYu4TN9CrZVy6nSAtE06IntyBV4toJIhorA=="],
"@react-aria/tabs/@react-aria/selection": ["@react-aria/selection@3.28.1", "", { "dependencies": { "@swc/helpers": "^0.5.0", "react-aria": "^3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "sha512-gGa9HkRnWsKxRhrtVASvecNyetMtP9fNF/Vcsy9Z+6NigjGUSN4SU0bhfglZ706B4t/NSMoVhcBJXlyFiG0hQw=="],
@@ -5788,7 +5786,7 @@
"@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
@@ -5902,6 +5900,8 @@
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"cmdk/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg=="],
"compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"conf/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
@@ -6026,6 +6026,8 @@
"jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"jwa/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"keytar/node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="],
@@ -6116,8 +6118,6 @@
"proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"puppeteer-core/@puppeteer/browsers": ["@puppeteer/browsers@2.6.1", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.6.3", "tar-fs": "^3.0.6", "unbzip2-stream": "^1.4.3", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg=="],
"radix-ui/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
"radix-ui/@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-collapsible": "1.1.16", "@radix-ui/react-collection": "1.1.12", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-direction": "1.1.2", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-controllable-state": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-BpZJNmetujnGgUI6OX0jEhEmlA46WPqgub8Rv09Kyquwd0cc1ndMKpiPYCjmBU6KSSRPAMtgLpEoZSG/tdNIWQ=="],
@@ -6278,6 +6278,8 @@
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
@@ -6306,8 +6308,6 @@
"unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"vaul/@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.19", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-context": "1.2.0", "@radix-ui/react-dismissable-layer": "1.1.15", "@radix-ui/react-focus-guards": "1.1.4", "@radix-ui/react-focus-scope": "1.1.12", "@radix-ui/react-id": "1.1.2", "@radix-ui/react-portal": "1.1.13", "@radix-ui/react-presence": "1.1.7", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-slot": "1.3.0", "@radix-ui/react-use-controllable-state": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.7.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg=="],
"verror/core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
"vite-node/es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
@@ -6894,6 +6894,22 @@
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
"conf/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
@@ -6904,8 +6920,6 @@
"d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
"duplexer2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"enquirer/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -6958,12 +6972,8 @@
"jest-util/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"jszip/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -7112,26 +7122,8 @@
"test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="],
"unzipper/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"vaul/@radix-ui/react-dialog/@radix-ui/primitive": ["@radix-ui/primitive@1.1.5", "", {}, "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.2.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.5", "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2", "@radix-ui/react-use-effect-event": "0.0.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.4", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.12", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3", "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-callback-ref": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.13", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.7", "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.7", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="],
"webview-ui/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"webview-ui/@vitejs/plugin-react-swc/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
@@ -7306,6 +7298,10 @@
"claude-dev/@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
"cmdk/@radix-ui/react-dialog/@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
"exceljs/archiver/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"exceljs/archiver/archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
@@ -7384,10 +7380,6 @@
"test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-dismissable-layer/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-scope/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw=="],
"webview-ui/vitest/@vitest/utils/loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
"webview-ui/vitest/chai/check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
@@ -7444,8 +7436,6 @@
"claude-dev/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
"exceljs/archiver/archiver-utils/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"exceljs/archiver/archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"exceljs/archiver/zip-stream/archiver-utils/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
+5
View File
@@ -1,5 +1,10 @@
# Cline SDK Changelog
## 0.0.64
- Improved max output token handling across providers (gateway routing, OpenAI vendor, and reasoning models)
- Frontmatter and user-instruction files that start with a UTF-8 byte order mark (e.g. saved by Windows editors) now parse correctly
## 0.0.63
- The session runtime now emits `task.mistake_limit_reached` telemetry when the consecutive-mistake limit is hit, so every host (CLI, VS Code extension, hub daemon) captures it — including auto-stops when no host prompt is configured
+1
View File
@@ -68,6 +68,7 @@
"!@cline/core/telemetry",
"!@cline/core/rpc",
"!@cline/shared/browser",
"!@cline/shared/node",
"!@cline/shared/types",
"!@cline/shared/storage",
"!@cline/shared/db"
@@ -13,6 +13,7 @@ import {
type AgentToolContext,
ClineCore,
createTool,
stripUtf8Bom,
} from "@cline/core";
import YAML from "yaml";
import { z } from "zod";
@@ -179,6 +180,9 @@ function parseFrontmatter(md: string): {
data: Record<string, unknown>;
body: string;
} {
// stripUtf8Bom keeps the frontmatter match below working for files saved with a leading
// UTF-8 BOM (see cline/cline#12151).
md = stripUtf8Bom(md);
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!m) return { data: {}, body: md.trim() };
try {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/agents",
"version": "0.0.63",
"version": "0.0.64",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
@@ -69,6 +69,50 @@ describe("AgentRuntime", () => {
expect(model.requests).toHaveLength(1);
});
it("fails a turn that hits the model output token limit before completion", async () => {
const logger = {
debug: vi.fn(),
log: vi.fn(),
error: vi.fn(),
};
const model = new ScriptedModel([
() => [
{ type: "reasoning-delta", text: "thinking..." },
{ type: "finish", reason: "max-tokens" },
],
]);
const runtime = new AgentRuntime({ model, logger });
const result = await runtime.run("Hi");
expect(result.status).toBe("failed");
expect(result.error?.message).toContain("maximum output token limit");
expect(model.requests).toHaveLength(1);
expect(result.messages).toHaveLength(2);
expect(result.messages.at(-1)).toMatchObject({
role: "assistant",
content: [{ type: "reasoning", text: "thinking..." }],
});
expect(logger.log).toHaveBeenCalledWith(
"Agent loop caught error",
expect.objectContaining({
severity: "error",
status: "failed",
errorMessage: expect.stringContaining("maximum output token limit"),
iteration: 1,
assistantContentPartCount: 1,
}),
);
expect(logger.error).toHaveBeenCalledWith(
"Agent run failed",
expect.objectContaining({
error: expect.objectContaining({
message: expect.stringContaining("maximum output token limit"),
}),
}),
);
});
it("does not persist an empty assistant message when the model stream fails", async () => {
const model = new ScriptedModel([
() => [{ type: "finish", reason: "error", error: "upstream failed" }],
@@ -1725,6 +1769,14 @@ describe("AgentRuntime", () => {
expect(result.status).toBe("failed");
expect(events).toContain("run-failed");
expect(logger.log).toHaveBeenCalledWith(
"Agent loop caught error",
expect.objectContaining({
severity: "error",
status: "failed",
errorMessage: "model failed",
}),
);
expect(logger.error).toHaveBeenCalled();
expect(telemetry.capture).toHaveBeenCalled();
});
+21 -5
View File
@@ -34,6 +34,9 @@ import {
} from "@cline/shared";
import { nanoid } from "nanoid";
const MAX_TOKENS_INCOMPLETE_TURN_MESSAGE =
"Model reached the maximum output token limit before completing the turn";
// Local `createUID` helper. The clinee source imports this from
// `@cline/shared` (see `packages/shared/dist/identifier.ts`), but
// sdk-re's shared package does not expose it yet. Inlining here keeps
@@ -645,6 +648,9 @@ export class AgentRuntime {
finishReason,
});
if (finishReason === "max-tokens" && toolCalls.length === 0) {
throw new Error(MAX_TOKENS_INCOMPLETE_TURN_MESSAGE);
}
if (finishReason === "error" && toolCalls.length === 0) {
throw new Error(this.state.lastError ?? "Model stream failed");
}
@@ -718,23 +724,33 @@ export class AgentRuntime {
const normalized =
error instanceof Error ? error : new Error(String(error));
const isControlledStop = normalized instanceof ControlledStopError;
const status =
this.abortController.signal.aborted || isControlledStop
? "aborted"
: "failed";
const isAborted = this.abortController.signal.aborted || isControlledStop;
const status = isAborted ? "aborted" : "failed";
this.state.status = status;
this.state.lastError = normalized.message;
const lastAssistantMessage = this.findLastAssistantMessage();
const result: AgentRunResult = {
agentId: this.state.agentId,
agentRole: this.state.agentRole,
runId: this.state.runId ?? createUID("run"),
status,
iterations: this.state.iteration,
outputText: textFromMessage(this.findLastAssistantMessage()),
outputText: textFromMessage(lastAssistantMessage),
messages: cloneMessages(this.state.messages),
usage: cloneUsage(this.state.usage),
error: status === "failed" ? normalized : undefined,
};
this.config.logger?.log?.("Agent loop caught error", {
severity: status === "failed" ? "error" : "warn",
agentId: this.state.agentId,
agentRole: this.state.agentRole,
runId: result.runId,
status,
iteration: this.state.iteration,
errorName: normalized.name,
errorMessage: normalized.message,
assistantContentPartCount: lastAssistantMessage?.content.length ?? 0,
});
await this.callAfterRunHooks(result);
if (status === "failed") {
await this.emit({
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/core",
"description": "Cline Core SDK for Node Runtime",
"version": "0.0.63",
"version": "0.0.64",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
@@ -86,6 +86,24 @@ ${content}`);
expect(updateSkillMarkdownEnabledState(content, true)).toBe(content);
});
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
// from being recognized when toggling a skill's enabled state.
it("disables a skill whose content starts with a UTF-8 BOM", () => {
const content = `\uFEFF---
name: code-review
description: Review code carefully
---
First line.`;
const updated = updateSkillMarkdownEnabledState(content, false);
const parsed = parseSkillConfigFromMarkdown(updated, "fallback");
expect(parsed.disabled).toBe(true);
expect(parsed.frontmatter.name).toBe("code-review");
expect(parsed.frontmatter.description).toBe("Review code carefully");
});
it("writes toggled content and returns the resulting state", async () => {
const tempRoot = await mkdtemp(join(tmpdir(), "core-skill-toggle-"));
tempRoots.push(tempRoot);
@@ -1,4 +1,5 @@
import { readFile, writeFile } from "node:fs/promises";
import { stripUtf8Bom } from "@cline/shared";
import YAML from "yaml";
export interface ToggleSkillFrontmatterOptions {
@@ -19,10 +20,15 @@ interface MarkdownFrontmatterParts {
}
function parseMarkdownFrontmatter(content: string): MarkdownFrontmatterParts {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
const normalizedContent = stripUtf8Bom(content);
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
const match = content.match(frontmatterRegex);
const match = normalizedContent.match(frontmatterRegex);
if (!match) {
return { data: {}, body: content, hadFrontmatter: false };
return { data: {}, body: normalizedContent, hadFrontmatter: false };
}
const [, yamlContent, body] = match;
@@ -137,6 +137,23 @@ Document rollout and rollback steps.`,
expect(workflow.disabled).toBe(true);
});
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
// from being recognized.
it("parses markdown frontmatter when the content starts with a UTF-8 BOM", () => {
const skill = parseSkillConfigFromMarkdown(
`\uFEFF---
name: my-skill
description: A test skill
---
This is a test skill.`,
"fallback",
);
expect(skill.name).toBe("my-skill");
expect(skill.description).toBe("A test skill");
expect(skill.instructions).toBe("This is a test skill.");
});
it("emits typed events for skills, rules, and workflows in one watcher", async () => {
const tempRoot = await mkdtemp(
join(tmpdir(), "core-user-instructions-loader-"),
@@ -1,5 +1,6 @@
import { readdir, readFile, stat } from "node:fs/promises";
import { basename, dirname, extname, join, resolve } from "node:path";
import { stripUtf8Bom } from "@cline/shared";
import {
AGENTS_RULES_FILE_NAME,
RULES_CONFIG_DIRECTORY_NAME,
@@ -193,10 +194,15 @@ async function discoverManagedPluginRoots(
function parseMarkdownFrontmatter(
content: string,
): ParseMarkdownFrontmatterResult {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// regex below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
const normalizedContent = stripUtf8Bom(content);
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
const match = content.match(frontmatterRegex);
const match = normalizedContent.match(frontmatterRegex);
if (!match) {
return { data: {}, body: content, hadFrontmatter: false };
return { data: {}, body: normalizedContent, hadFrontmatter: false };
}
const [, yamlContent, body] = match;
@@ -211,7 +217,7 @@ function parseMarkdownFrontmatter(
const message = error instanceof Error ? error.message : String(error);
return {
data: {},
body: content,
body: normalizedContent,
hadFrontmatter: true,
parseError: message,
};
@@ -23,6 +23,23 @@ You are a code reviewer.`);
});
});
// Regression test for https://github.com/cline/cline/issues/12151: a leading UTF-8 BOM
// (e.g. saved by Windows Notepad's "UTF-8 with BOM" encoding) must not prevent frontmatter
// from being recognized.
it("parses frontmatter when the content starts with a UTF-8 BOM", () => {
const config = parseConfiguredAgentConfig(`\uFEFF---
name: code-reviewer
description: Reviews code
---
You are a code reviewer.`);
expect(config).toMatchObject({
name: "code-reviewer",
description: "Reviews code",
systemPrompt: "You are a code reviewer.",
});
});
it("does not treat delimiter lines in the body as frontmatter delimiters", () => {
const config = parseConfiguredAgentConfig(`---
name: code-reviewer
@@ -1,5 +1,6 @@
import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
import { basename, extname, join } from "node:path";
import { stripUtf8Bom } from "@cline/shared";
import { resolveAgentConfigSearchPaths } from "@cline/shared/storage";
import YAML from "yaml";
import { z } from "zod";
@@ -40,6 +41,11 @@ function splitFrontmatter(content: string): {
frontmatter: string;
body: string;
} {
// Strip a leading UTF-8 BOM (e.g. added by Windows Notepad's "UTF-8 with BOM" encoding),
// which Node's `utf-8` decoding does not strip on its own. Without this the frontmatter
// match below never matches a file that starts with "\uFEFF---" (see cline/cline#12151).
content = stripUtf8Bom(content);
const firstLineMatch = content.match(/^(---)[^\S\r\n]*(?:\r?\n|$)/);
if (!firstLineMatch) {
throw new Error("Missing YAML frontmatter block in agent config file.");
+1
View File
@@ -112,6 +112,7 @@ export {
parseUserCommandEnvelope,
registerDisposable,
SDK_ERROR_TELEMETRY_EVENT,
stripUtf8Bom,
} from "@cline/shared";
export * from "@cline/shared/storage";
export {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/llms",
"version": "0.0.63",
"version": "0.0.64",
"description": "Config-driven SDK for selecting, extending, and instantiating LLM providers and models",
"repository": {
"type": "git",
+8 -5
View File
@@ -77,13 +77,16 @@ while generating the catalog. Conceptually:
safeOutputTokens = min(
modelReportedMaxOutput,
contextWindow - estimatedPromptTokens - reserveTokens,
userConfiguredOutputCap,
userConfiguredOutputCap or productDefaultOutputCap,
)
```
The SDK gateway only sends an output token limit when the caller provides
`request.options.maxTokens` or an equivalent host configuration. Catalog
metadata does not become a request parameter by itself.
The SDK gateway resolves an output token limit for the provider request. It uses
`request.options.maxTokens` or an equivalent host configuration when present,
otherwise it applies the product default output cap
(`DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS`, currently 32000) when the model catalog has
either an output limit or a context window. Provider modules are responsible for
forwarding that limit only to wire API surfaces that support it.
The exact request-limit policy belongs in the provider/gateway/core request
path, not in generated catalog data.
@@ -161,5 +164,5 @@ and observable.
- `catalog-live.test.ts`: tests catalog normalization behavior.
- `catalog.generated.ts`: checked-in generated provider/model catalog.
- `../../scripts/generate-models.ts`: writes generated catalog output.
- `../providers/ai-sdk.ts`: passes `maxOutputTokens` into AI SDK.
- `../providers/ai-sdk.ts`: conditionally passes `maxOutputTokens` into AI SDK.
- `../providers/gateway.ts`: resolves per-request/default `maxTokens`.
+25 -9
View File
@@ -15,7 +15,7 @@ import {
parseJsonStream,
sanitizeSurrogates,
} from "@cline/shared";
import { jsonSchema, NoSuchToolError, streamText } from "ai";
import { type CallSettings, jsonSchema, NoSuchToolError, streamText } from "ai";
import { nanoid } from "nanoid";
import { extractErrorMessage } from "./format";
import {
@@ -53,6 +53,18 @@ interface GatewayNormalizedUsage {
}
type ProviderModuleKind = AiSdkProviderOptionsTarget;
export function buildAiSdkStreamConfig(
request: GatewayStreamRequest,
_context: GatewayProviderContext,
): Partial<CallSettings> {
return {
...(request.maxTokens !== undefined
? { maxOutputTokens: request.maxTokens }
: {}),
temperature: request.temperature,
};
}
function buildCachedAiSdkMessages(
request: GatewayStreamRequest,
context: GatewayProviderContext,
@@ -352,10 +364,15 @@ function toAiSdkMessages(
}
}
// A message left empty only because its reasoning was dropped is
// omitted entirely instead of forwarded as an empty turn.
const emptiedByDroppedReasoning = !includeReasoning && skippedReasoning;
if (content.length > 0) {
normalizedMessages.push({ role: message.role, content });
} else if (!includeReasoning && skippedReasoning) {
} else if (message.role === "user" || message.role === "assistant") {
} else if (
!emptiedByDroppedReasoning &&
(message.role === "user" || message.role === "assistant")
) {
normalizedMessages.push({ role: message.role, content: "" });
}
}
@@ -1178,6 +1195,9 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
context,
kind,
) as never;
const requestConfig = provider.buildStreamConfig
? provider.buildStreamConfig(request, context)
: buildAiSdkStreamConfig(request, context);
recordProviderRequestCapture({
stage: "ai_sdk_prompt",
request,
@@ -1186,8 +1206,7 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
...(useSystemOption ? { system: systemPrompt } : {}),
tools,
providerOptions,
maxOutputTokens: request.maxTokens,
temperature: request.temperature,
...requestConfig,
},
});
stream = streamText({
@@ -1195,16 +1214,13 @@ function createAiSdkProvider(kind: ProviderModuleKind): GatewayProviderFactory {
messages: messages as never,
...(useSystemOption ? { system: systemPrompt } : {}),
tools: tools as never,
temperature: request.temperature,
...(request.maxTokens !== undefined
? { maxOutputTokens: request.maxTokens }
: {}),
abortSignal: request.signal,
experimental_repairToolCall: repairMalformedToolCall as never,
experimental_telemetry: {
isEnabled: langfuse,
},
providerOptions,
...requestConfig,
onError: ({ error: streamError }) => {
const msg = extractErrorMessage(streamError);
capturedError.current = msg;
@@ -5,6 +5,7 @@ import {
toGatewayRequestMessages,
} from "./compat";
import { ClineNotSubscribedError } from "./errors";
import { DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS } from "./gateway";
import type { Message } from "./types";
const streamTextSpy = vi.fn();
@@ -361,7 +362,7 @@ describe("createGatewayApiHandler.createMessage", () => {
openaiCompatibleSpy.mockClear();
});
it("does not convert catalog maxTokens into request maxOutputTokens", async () => {
it("uses the default maxOutputTokens without expanding to catalog maxTokens", async () => {
streamTextSpy.mockReturnValue({
fullStream: (async function* () {
yield { type: "finish", finishReason: "stop" };
@@ -395,7 +396,10 @@ describe("createGatewayApiHandler.createMessage", () => {
const call = streamTextSpy.mock.calls.at(-1)?.[0] as
| { maxOutputTokens?: unknown }
| undefined;
expect(call).not.toHaveProperty("maxOutputTokens");
expect(call).toHaveProperty(
"maxOutputTokens",
DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS,
);
});
it("sends configured OpenAI-compatible maxOutputTokens to the provider request", async () => {
+103 -6
View File
@@ -15,7 +15,11 @@ import {
} from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { normalizeModelsDevProviderModels } from "../catalog/catalog-live";
import { createGateway, resolveGatewayRequestMaxTokens } from "./gateway";
import {
createGateway,
DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS,
resolveGatewayRequestMaxTokens,
} from "./gateway";
const streamTextSpy = vi.fn();
const openaiCompatibleFactorySpy = vi.fn();
@@ -218,14 +222,46 @@ describe("sdk-gateway", () => {
}
});
it("does not synthesize request max tokens from catalog metadata", () => {
it("uses the old default output cap when request max tokens are omitted", () => {
expect(
resolveGatewayRequestMaxTokens({
requestedMaxTokens: undefined,
model: { maxOutputTokens: 202_800, contextWindow: 202_800 },
estimatedInputTokens: 1_000,
}),
).toBeUndefined();
).toBe(DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS);
});
it("lifts the default output cap above an explicit reasoning budget", () => {
expect(
resolveGatewayRequestMaxTokens({
requestedMaxTokens: undefined,
reasoningBudgetTokens: 50_000,
model: { maxOutputTokens: 202_800, contextWindow: 202_800 },
estimatedInputTokens: 1_000,
outputReserveTokens: 1_024,
}),
).toBe(51_024);
// Still clamped by the model's max output tokens.
expect(
resolveGatewayRequestMaxTokens({
requestedMaxTokens: undefined,
reasoningBudgetTokens: 50_000,
model: { maxOutputTokens: 40_000, contextWindow: 202_800 },
estimatedInputTokens: 1_000,
}),
).toBe(40_000);
// Explicit request max tokens still win over the reasoning floor.
expect(
resolveGatewayRequestMaxTokens({
requestedMaxTokens: 8_192,
reasoningBudgetTokens: 50_000,
model: { maxOutputTokens: 202_800, contextWindow: 202_800 },
estimatedInputTokens: 1_000,
}),
).toBe(8_192);
});
it("resolves explicit request max tokens from model and context caps", () => {
@@ -290,10 +326,10 @@ describe("sdk-gateway", () => {
expect(estimatedTokens).toBeGreaterThan(4_000);
});
it("does not apply catalog output caps when the request omits max tokens", async () => {
it("applies the old default output cap when the request omits max tokens", async () => {
const createProvider = vi.fn(() => ({
async *stream(request: { maxTokens?: number }) {
expect(request.maxTokens).toBeUndefined();
expect(request.maxTokens).toBe(DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS);
yield { type: "finish", reason: "stop" } satisfies AgentModelEvent;
},
}));
@@ -582,6 +618,41 @@ describe("sdk-gateway", () => {
}),
});
expect(events.at(-1)).toEqual({ type: "finish", reason: "tool-calls" });
const call = streamTextSpy.mock.calls.at(-1)?.[0] as
| { maxOutputTokens?: unknown }
| undefined;
expect(call).not.toHaveProperty("maxOutputTokens");
});
it("sends explicit maxOutputTokens through the OpenAI Responses provider", async () => {
streamTextSpy.mockReturnValue({
fullStream: makeStreamParts([
{ type: "finish", usage: { inputTokens: 1, outputTokens: 1 } },
]),
});
const gateway = createGateway({
providerConfigs: [
{
providerId: "openai-native",
apiKey: "test",
},
],
});
await collect(
await gateway.stream({
providerId: "openai-native",
modelId: "gpt-5-mini",
messages: baseMessages,
maxTokens: 8_192,
}),
);
const call = streamTextSpy.mock.calls.at(-1)?.[0] as
| { maxOutputTokens?: unknown }
| undefined;
expect(call?.maxOutputTokens).toBe(8_192);
});
it("surfaces nested AI SDK stream errors as human-readable finish messages", async () => {
@@ -2249,6 +2320,32 @@ describe("sdk-gateway", () => {
expect(call).not.toHaveProperty("maxOutputTokens");
});
it("does not send explicit maxOutputTokens to ChatGPT OAuth", async () => {
streamTextSpy.mockReturnValue({
fullStream: makeStreamParts([
{ type: "finish", usage: { inputTokens: 1, outputTokens: 1 } },
]),
});
const gateway = createGateway({
providerConfigs: [{ providerId: "openai-codex" }],
});
await collect(
await gateway.stream({
providerId: "openai-codex",
modelId: "gpt-5.4",
messages: baseMessages,
maxTokens: 8_192,
}),
);
const call = streamTextSpy.mock.calls.at(-1)?.[0] as
| { maxOutputTokens?: unknown }
| undefined;
expect(call).not.toHaveProperty("maxOutputTokens");
});
it("passes Codex instructions through provider options and removes the system message from messages", async () => {
streamTextSpy.mockReturnValue({
fullStream: makeStreamParts([
@@ -3496,7 +3593,7 @@ describe("sdk-gateway", () => {
reasoning: { enabled: true },
}),
openrouter: expect.objectContaining({
reasoning: { enabled: true },
reasoning: { enabled: true, max_tokens: 19_200 },
}),
}),
}),
+43 -25
View File
@@ -15,9 +15,11 @@ import { estimateRequestInputTokens } from "@cline/shared";
import { toAsyncIterable } from "./async";
import { BUILTIN_PROVIDER_REGISTRATIONS } from "./builtins-runtime";
import { GatewayRegistry } from "./registry";
import { isPositiveFiniteNumber } from "./utils";
export type * from "@cline/shared";
export const DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS = 32_000;
const GATEWAY_OUTPUT_RESERVE_TOKENS = 1_024;
function mergeRequestMetadata(
@@ -115,27 +117,42 @@ class GatewayModelAdapter implements AgentModel {
}
}
function isPositiveFiniteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
export function resolveGatewayRequestMaxTokens(input: {
requestedMaxTokens?: number;
model: Pick<GatewayModelDefinition, "contextWindow" | "maxOutputTokens">;
estimatedInputTokens: number;
defaultMaxOutputTokens?: number;
outputReserveTokens?: number;
reasoningBudgetTokens?: number;
onContextOverflow?: (details: {
contextWindow: number;
estimatedInputTokens: number;
reserveTokens: number;
}) => void;
}): number | undefined {
if (!isPositiveFiniteNumber(input.requestedMaxTokens)) {
return undefined;
const caps: number[] = [];
if (isPositiveFiniteNumber(input.requestedMaxTokens)) {
caps.push(Math.floor(input.requestedMaxTokens));
} else {
// Providers like Anthropic require max_tokens to exceed the thinking
// budget, so an explicit reasoning budget lifts the synthesized default
// (still clamped by model max output and remaining context below).
const reasoningFloor = isPositiveFiniteNumber(input.reasoningBudgetTokens)
? Math.floor(input.reasoningBudgetTokens) +
(input.outputReserveTokens ?? GATEWAY_OUTPUT_RESERVE_TOKENS)
: 0;
const defaultMaxOutputTokens = Math.max(
input.defaultMaxOutputTokens ?? DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS,
reasoningFloor,
);
if (
isPositiveFiniteNumber(input.model.maxOutputTokens) ||
isPositiveFiniteNumber(input.model.contextWindow)
) {
caps.push(defaultMaxOutputTokens);
}
}
const caps: number[] = [Math.floor(input.requestedMaxTokens)];
if (isPositiveFiniteNumber(input.model.maxOutputTokens)) {
caps.push(Math.floor(input.model.maxOutputTokens));
}
@@ -234,30 +251,31 @@ export class DefaultGateway implements Gateway {
request.providerId,
);
const provider = await providerRecord.createProvider(providerRecord.config);
const maxTokens = isPositiveFiniteNumber(request.maxTokens)
? resolveGatewayRequestMaxTokens({
requestedMaxTokens: request.maxTokens,
model: resolved.model,
estimatedInputTokens: estimateRequestInputTokens(request),
onContextOverflow: (details) => {
this.logger?.log(
"Estimated prompt tokens exceed model context window",
{
severity: "warn",
providerId: resolved.provider.id,
modelId: resolved.model.id,
...details,
},
);
const maxTokens = resolveGatewayRequestMaxTokens({
requestedMaxTokens: request.maxTokens,
model: resolved.model,
estimatedInputTokens: estimateRequestInputTokens(request),
reasoningBudgetTokens: request.reasoning?.budgetTokens,
onContextOverflow: (details) => {
this.logger?.log(
"Estimated prompt tokens exceed model context window",
{
severity: "warn",
providerId: resolved.provider.id,
modelId: resolved.model.id,
...details,
},
})
: undefined;
);
},
});
const stream = await provider.stream(
{
...request,
modelId: resolved.model.id,
providerId: resolved.provider.id,
maxTokens,
defaultedMaxTokens:
maxTokens !== undefined && !isPositiveFiniteNumber(request.maxTokens),
},
{
provider: resolved.provider,
@@ -283,7 +283,7 @@ const openRouterReasoningRule: ProviderOptionRule = {
build: (input) =>
buildReasoningPatchForProvider(
input,
buildOpenRouterReasoningOptions(input.request),
buildOpenRouterReasoningOptions(input.request, input.context),
),
};
@@ -20,6 +20,7 @@ type ContextOverrides = {
providerId?: string;
modelId?: string;
family?: string;
maxOutputTokens?: number;
modelMetadata?: NonNullable<GatewayProviderContext["model"]["metadata"]>;
capabilities?: GatewayProviderContext["model"]["capabilities"];
metadata?: GatewayProviderContext["provider"]["metadata"];
@@ -87,6 +88,7 @@ function makeContext(options?: ContextOverrides): GatewayProviderContext {
id: modelId,
name: modelId,
providerId,
maxOutputTokens: options?.maxOutputTokens,
capabilities: options?.capabilities,
metadata: modelMetadata,
},
@@ -605,7 +607,7 @@ describe("composeAiSdkProviderOptions: family/provider thinking patches", () =>
expect: [
{
bucket: "openrouter",
has: { reasoning: { enabled: true } },
has: { reasoning: { enabled: true, max_tokens: 19_200 } },
lacks: ["thinking", "effort", "reasoningEffort"],
},
{
@@ -614,6 +616,65 @@ describe("composeAiSdkProviderOptions: family/provider thinking patches", () =>
},
],
},
{
name: "openrouter reasoning enabled-only uses resolved output cap for reasoning budget",
request: {
providerId: "openrouter",
modelId: "openai/gpt-oss-120b",
maxTokens: 10_000,
reasoning: { enabled: true },
},
expect: [
{
bucket: "openrouter",
has: { reasoning: { enabled: true, max_tokens: 6_000 } },
lacks: ["thinking", "effort", "reasoningEffort"],
},
{
bucket: "openaiCompatible",
lacks: ["thinking", "reasoning", "effort", "reasoningEffort"],
},
],
},
{
name: "openrouter reasoning enabled-only uses model output cap when request cap is absent",
request: {
providerId: "openrouter",
modelId: "openai/gpt-oss-120b",
reasoning: { enabled: true },
},
context: { maxOutputTokens: 12_000 },
expect: [
{
bucket: "openrouter",
has: { reasoning: { enabled: true, max_tokens: 7_200 } },
lacks: ["thinking", "effort", "reasoningEffort"],
},
{
bucket: "openaiCompatible",
lacks: ["thinking", "reasoning", "effort", "reasoningEffort"],
},
],
},
{
name: "openrouter reasoning effort sends only effort (no max_tokens, which OpenRouter rejects alongside effort)",
request: {
providerId: "openrouter",
modelId: "openai/gpt-oss-120b",
reasoning: { effort: "high" },
},
expect: [
{
bucket: "openrouter",
has: { reasoning: { effort: "high" } },
lacks: ["thinking", "reasoningEffort"],
},
{
bucket: "openaiCompatible",
lacks: ["thinking", "reasoning", "effort", "reasoningEffort"],
},
],
},
{
name: "openrouter unset reasoning -> no reasoning field",
request: {
@@ -665,7 +726,7 @@ describe("composeAiSdkProviderOptions: family/provider thinking patches", () =>
expect: [
{
bucket: "openrouter",
has: { reasoning: { enabled: true } },
has: { reasoning: { enabled: true, max_tokens: 19_200 } },
lacks: ["thinking"],
},
{
@@ -1307,7 +1368,7 @@ describe("composeAiSdkProviderOptions: family/provider thinking patches", () =>
expect: [
{
bucket: "openrouter",
has: { reasoning: { enabled: true } },
has: { reasoning: { enabled: true, max_tokens: 19_200 } },
lacks: ["thinking", "effort", "reasoningEffort"],
},
{
@@ -1,4 +1,16 @@
import type { GatewayStreamRequest } from "@cline/shared";
import type {
GatewayProviderContext,
GatewayStreamRequest,
} from "@cline/shared";
import { DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS } from "../gateway";
import { isPositiveFiniteNumber } from "../utils";
// OpenRouter separates total output limits (`max_tokens` / `max_output_tokens`)
// from `reasoning.max_tokens`, which caps only the reasoning-token portion.
// Sources:
// - https://openrouter.ai/docs/api/reference/parameters
// - https://openrouter.ai/docs/api/reference/responses/reasoning
const OPENROUTER_REASONING_BUDGET_FRACTION = 0.6;
export function hasReasoningControls(
reasoning: GatewayStreamRequest["reasoning"],
@@ -10,8 +22,24 @@ export function hasReasoningControls(
);
}
function resolveOpenRouterReasoningMaxTokens(
request: GatewayStreamRequest,
context?: GatewayProviderContext,
): number {
const outputMaxTokens = isPositiveFiniteNumber(request.maxTokens)
? request.maxTokens
: isPositiveFiniteNumber(context?.model.maxOutputTokens)
? context.model.maxOutputTokens
: DEFAULT_GATEWAY_MAX_OUTPUT_TOKENS;
return Math.max(
1,
Math.floor(outputMaxTokens * OPENROUTER_REASONING_BUDGET_FRACTION),
);
}
export function buildOpenRouterReasoningOptions(
request: GatewayStreamRequest,
context?: GatewayProviderContext,
): Record<string, unknown> | undefined {
const reasoning = request.reasoning;
if (!hasReasoningControls(reasoning)) {
@@ -22,8 +50,13 @@ export function buildOpenRouterReasoningOptions(
return { effort: "none" };
}
// OpenRouter accepts one reasoning control mode. Preserve this precedence:
// explicit disable, exact token budget, effort level, then plain enable.
// AI SDK `maxOutputTokens` still caps the whole response. This provider option
// reserves room within that response by capping OpenRouter reasoning tokens.
// Preserve explicit reasoning budgets when present; otherwise derive the cap
// from the resolved request budget, model catalog output limit, or default.
// OpenRouter rejects requests carrying both `reasoning.effort` and
// `reasoning.max_tokens`, so the effort branch sends only `effort`.
// DOCS: https://openrouter.ai/docs/api/reference/responses/reasoning
if (typeof reasoning?.budgetTokens === "number") {
return { max_tokens: reasoning.budgetTokens };
}
@@ -33,7 +66,10 @@ export function buildOpenRouterReasoningOptions(
}
if (reasoning?.enabled === true) {
return { enabled: true };
return {
enabled: true,
max_tokens: resolveOpenRouterReasoningMaxTokens(request, context),
};
}
return undefined;
+3
View File
@@ -0,0 +1,3 @@
export function isPositiveFiniteNumber(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value > 0;
}
+130
View File
@@ -0,0 +1,130 @@
import type {
GatewayResolvedProviderConfig,
GatewayStreamRequest,
} from "@cline/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createOpenAIProviderModule } from "./openai";
const createOpenAIMock = vi.hoisted(() => vi.fn());
const responsesModelMock = vi.hoisted(() =>
vi.fn((modelId: string) => ({ provider: "openai", modelId })),
);
vi.mock("@ai-sdk/openai", () => ({
createOpenAI: createOpenAIMock,
}));
describe("createOpenAIProviderModule", () => {
beforeEach(() => {
createOpenAIMock.mockReset();
createOpenAIMock.mockReturnValue({
responses: responsesModelMock,
});
responsesModelMock.mockClear();
});
it("forwards maxOutputTokens for explicit caps from direct callers", async () => {
const provider = await createOpenAIProviderModule(config(), context());
const streamConfig = provider.buildStreamConfig?.(
request({ maxTokens: 8_192 }),
context(),
);
expect(streamConfig?.maxOutputTokens).toBe(8_192);
});
it("forwards maxOutputTokens for gateway-resolved explicit caps", async () => {
const provider = await createOpenAIProviderModule(config(), context());
const streamConfig = provider.buildStreamConfig?.(
request({ maxTokens: 8_192, defaultedMaxTokens: false }),
context(),
);
expect(streamConfig?.maxOutputTokens).toBe(8_192);
});
it("drops gateway-synthesized default caps", async () => {
const provider = await createOpenAIProviderModule(config(), context());
const streamConfig = provider.buildStreamConfig?.(
request({ maxTokens: 32_000, defaultedMaxTokens: true }),
context(),
);
expect(streamConfig).not.toHaveProperty("maxOutputTokens");
});
it("drops explicit caps for the ChatGPT OAuth backend", async () => {
const provider = await createOpenAIProviderModule(
config({ baseUrl: "https://chatgpt.com/backend-api/codex" }),
context(),
);
const streamConfig = provider.buildStreamConfig?.(
request({ maxTokens: 8_192 }),
context(),
);
expect(streamConfig).not.toHaveProperty("maxOutputTokens");
});
it("does not treat non-chatgpt.com hosts containing 'chatgpt.com' as OAuth", async () => {
for (const baseUrl of [
"https://example.com/chatgpt.com",
"https://chatgpt.com.example.com/v1",
"https://notchatgpt.com/v1",
]) {
const provider = await createOpenAIProviderModule(
config({ baseUrl }),
context(),
);
const streamConfig = provider.buildStreamConfig?.(
request({ maxTokens: 8_192 }),
context(),
);
expect(streamConfig?.maxOutputTokens).toBe(8_192);
}
});
});
function config(
overrides: Partial<GatewayResolvedProviderConfig> = {},
): GatewayResolvedProviderConfig {
return {
providerId: "openai-native",
apiKey: "test-api-key",
...overrides,
};
}
function context() {
return {
provider: {
id: "openai-native",
name: "OpenAI",
defaultModelId: "gpt-5-mini",
models: [],
},
model: {
providerId: "openai-native",
id: "gpt-5-mini",
name: "gpt-5-mini",
},
config: config(),
};
}
function request(
overrides: Partial<GatewayStreamRequest>,
): GatewayStreamRequest {
return {
providerId: "openai-native",
modelId: "gpt-5-mini",
messages: [],
...overrides,
};
}
+27
View File
@@ -6,6 +6,18 @@ import type {
import { resolveApiKey } from "../http";
import type { ProviderFactoryResult } from "./types";
function isChatGptOAuthBaseUrl(baseUrl: string | undefined): boolean {
if (!baseUrl) {
return false;
}
try {
const { hostname } = new URL(baseUrl);
return hostname === "chatgpt.com" || hostname.endsWith(".chatgpt.com");
} catch {
return false;
}
}
export async function createOpenAIProviderModule(
config: GatewayResolvedProviderConfig,
context: GatewayProviderContext,
@@ -18,7 +30,22 @@ export async function createOpenAIProviderModule(
fetch: config.fetch,
name: context.provider.id,
});
// The ChatGPT OAuth Codex backend rejects `max_output_tokens`, and the
// OpenAI Responses API applies its own defaults, so gateway-synthesized
// caps are never forwarded. Explicit caps — whether resolved by the
// gateway from a caller request or passed straight to this provider —
// are honored for API-key usage because that endpoint supports output
// limits.
const isChatGptOAuth = isChatGptOAuthBaseUrl(config.baseUrl);
return {
model: (modelId) => provider.responses(modelId),
buildStreamConfig: (request) => ({
...(!isChatGptOAuth &&
request.maxTokens !== undefined &&
request.defaultedMaxTokens !== true
? { maxOutputTokens: request.maxTokens }
: {}),
temperature: request.temperature,
}),
};
}
+10
View File
@@ -1,5 +1,15 @@
import type {
GatewayProviderContext,
GatewayStreamRequest,
} from "@cline/shared";
import type { CallSettings } from "ai";
export interface ProviderFactoryResult {
model: (modelId: string) => unknown;
buildStreamConfig?: (
request: GatewayStreamRequest,
context: GatewayProviderContext,
) => Partial<CallSettings>;
}
export interface AiSdkStreamPart {
@@ -7,6 +7,6 @@
"response": "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-sonnet-4-6\",\"id\":\"ID_REDACTED\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"stop_details\":null,\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":1,\"service_tier\":\"standard\",\"inference_geo\":\"global\"}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: ping\ndata: {\"type\":\"ping\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"OK\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"stop_details\":null},\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":4}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream; charset=utf-8",
"requestBody": "{\"max_tokens\":128000,\"messages\":[{\"content\":[{\"cache_control\":{\"type\":\"ephemeral\"},\"text\":\"Reply with the single word OK.\",\"type\":\"text\"}],\"role\":\"user\"}],\"model\":\"claude-sonnet-4-6\",\"stream\":true,\"system\":[{\"text\":\"You are a concise assistant.\",\"type\":\"text\"}]}"
"requestBody": "{\"max_tokens\":32000,\"messages\":[{\"content\":[{\"cache_control\":{\"type\":\"ephemeral\"},\"text\":\"Reply with the single word OK.\",\"type\":\"text\"}],\"role\":\"user\"}],\"model\":\"claude-sonnet-4-6\",\"stream\":true,\"system\":[{\"text\":\"You are a concise assistant.\",\"type\":\"text\"}]}"
}
]
@@ -7,6 +7,6 @@
"response": "data: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"REDACTED\"}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"OK\"},\"logprobs\":null,\"finish_reason\":null}],\"system_fingerprint\":\"REDACTED\"}\n\ndata: {\"id\":\"ID_REDACTED\",\"object\":\"chat.completion.chunk\",\"created\":0,\"model\":\"anthropic/claude-sonnet-4.6\",\"choices\":[{\"index\":0,\"delta\":{\"provider_metadata\":{\"anthropic\":{\"usage\":{\"input_tokens\":22,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":4,\"service_tier\":\"standard\",\"inference_geo\":\"global\"},\"cacheCreationInputTokens\":0,\"stopSequence\":null,\"iterations\":null,\"container\":null,\"contextManagement\":null},\"gateway\":{\"routing\":{\"originalModelId\":\"anthropic/claude-sonnet-4.6\",\"resolvedProvider\":\"anthropic\",\"fallbacksAvailable\":[\"vertexAnthropic\",\"bedrock\"],\"planningReasoning\":\"REDACTED\",\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"finalProvider\":\"anthropic\",\"modelAttemptCount\":1,\"modelAttempts\":[{\"canonicalSlug\":\"anthropic/claude-sonnet-4.6\",\"success\":true,\"providerAttemptCount\":1,\"providerAttempts\":[{\"provider\":\"anthropic\",\"credentialType\":\"byok\",\"success\":true,\"startTime\":0,\"endTime\":0,\"providerRequestId\":\"ID_REDACTED\",\"statusCode\":200,\"providerResponseId\":\"ID_REDACTED\"}]}],\"totalProviderAttemptCount\":1},\"cost\":\"0\",\"marketCost\":\"0.000126\",\"surchargeCost\":\"0\",\"gatewayCost\":\"0\",\"generationId\":\"ID_REDACTED\"}}},\"logprobs\":null,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":22,\"completion_tokens\":4,\"total_tokens\":26,\"cost\":0,\"is_byok\":true,\"prompt_tokens_details\":{\"cached_tokens\":0,\"audio_tokens\":0,\"video_tokens\":0},\"cost_details\":{\"upstream_inference_cost\":0.000126,\"upstream_inference_prompt_cost\":0,\"upstream_inference_completions_cost\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0},\"cache_creation_input_tokens\":0,\"market_cost\":0.000126},\"system_fingerprint\":\"REDACTED\",\"generationId\":\"ID_REDACTED\"}\n\ndata: [DONE]\n\n",
"responseIsBinary": false,
"contentType": "text/event-stream",
"requestBody": "{\"cache_control\":{\"type\":\"ephemeral\"},\"messages\":[{\"content\":\"You are a concise assistant.\",\"role\":\"system\"},{\"cache_control\":{\"type\":\"ephemeral\"},\"content\":\"Reply with the single word OK.\",\"role\":\"user\"}],\"model\":\"anthropic/claude-sonnet-4.6\",\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
"requestBody": "{\"cache_control\":{\"type\":\"ephemeral\"},\"max_tokens\":32000,\"messages\":[{\"content\":\"You are a concise assistant.\",\"role\":\"system\"},{\"cache_control\":{\"type\":\"ephemeral\"},\"content\":\"Reply with the single word OK.\",\"role\":\"user\"}],\"model\":\"anthropic/claude-sonnet-4.6\",\"stream\":true,\"stream_options\":{\"include_usage\":true}}"
}
]
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cline/sdk",
"description": "Cline SDK - user-facing alias for @cline/core",
"version": "0.0.63",
"version": "0.0.64",
"repository": {
"type": "git",
"url": "https://github.com/cline/cline",
+1
View File
@@ -37,6 +37,7 @@ await runBuild("node", {
"./src/index.ts",
"./src/automation/index.ts",
"./src/db/index.ts",
"./src/node.ts",
"./src/remote-config/index.ts",
"./src/storage/index.ts",
],
+5 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@cline/shared",
"version": "0.0.63",
"version": "0.0.64",
"description": "Shared utilities, types, and schemas for Cline packages",
"repository": {
"type": "git",
@@ -33,6 +33,10 @@
"types": "./dist/db/index.d.ts",
"import": "./dist/db/index.js"
},
"./node": {
"types": "./dist/node.d.ts",
"import": "./dist/node.js"
},
"./automation": {
"types": "./dist/automation/index.d.ts",
"import": "./dist/automation/index.js"
+1
View File
@@ -224,6 +224,7 @@ export { getDefaultShell, getShellArgs } from "./parse/shell";
export {
maskSecret,
sanitizeFileName,
stripUtf8Bom,
trimNonEmpty,
truncateSplit,
truncateStr,
+1
View File
@@ -238,6 +238,7 @@ export { getDefaultShell, getShellArgs } from "./parse/shell";
export {
maskSecret,
sanitizeFileName,
stripUtf8Bom,
trimNonEmpty,
truncateSplit,
truncateStr,
+8
View File
@@ -166,6 +166,14 @@ export interface GatewayStreamRequest {
tools?: readonly AgentToolDefinition[];
temperature?: number;
maxTokens?: number;
/**
* Set by the gateway when `maxTokens` was synthesized from gateway/model
* defaults rather than derived from an explicit caller cap. Providers can
* use this to avoid forwarding synthesized caps to backends that reject
* them, while still honoring explicit caps from any caller including
* ones that reach the provider without going through the gateway.
*/
defaultedMaxTokens?: boolean;
metadata?: Record<string, unknown>;
reasoning?: {
enabled?: boolean;
+44
View File
@@ -0,0 +1,44 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { readFileStrippingUtf8Bom, readFileSyncStrippingUtf8Bom } from "./node";
describe("UTF-8 file readers", () => {
const tempDirectories: string[] = [];
afterEach(async () => {
await Promise.all(
tempDirectories
.splice(0)
.map((directory) => rm(directory, { recursive: true, force: true })),
);
});
async function writeTempFile(content: string): Promise<string> {
const directory = await mkdtemp(join(tmpdir(), "cline-utf8-file-"));
tempDirectories.push(directory);
const filePath = join(directory, "example.txt");
await writeFile(filePath, content, "utf8");
return filePath;
}
it("strips a leading BOM in synchronous reads", async () => {
const filePath = await writeTempFile("\uFEFFcontent");
expect(readFileSyncStrippingUtf8Bom(filePath)).toBe("content");
});
it("strips a leading BOM in asynchronous reads", async () => {
const filePath = await writeTempFile("\uFEFFcontent");
await expect(readFileStrippingUtf8Bom(filePath)).resolves.toBe("content");
});
it("preserves interior BOM characters", async () => {
const filePath = await writeTempFile("a\uFEFFb");
expect(readFileSyncStrippingUtf8Bom(filePath)).toBe("a\uFEFFb");
await expect(readFileStrippingUtf8Bom(filePath)).resolves.toBe("a\uFEFFb");
});
});
+18
View File
@@ -0,0 +1,18 @@
import { readFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { stripUtf8Bom } from "./parse/string";
type ReadFileSyncPath = Parameters<typeof readFileSync>[0];
type ReadFilePath = Parameters<typeof readFile>[0];
/** Read a UTF-8 text file and remove its optional leading byte order mark. */
export function readFileSyncStrippingUtf8Bom(path: ReadFileSyncPath): string {
return stripUtf8Bom(readFileSync(path, "utf8"));
}
/** Read a UTF-8 text file and remove its optional leading byte order mark. */
export async function readFileStrippingUtf8Bom(
path: ReadFilePath,
): Promise<string> {
return stripUtf8Bom(await readFile(path, "utf8"));
}
+21 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { trimNonEmpty } from "./string";
import { stripUtf8Bom, trimNonEmpty } from "./string";
describe("trimNonEmpty", () => {
it("returns trimmed strings and omits empty values", () => {
@@ -10,3 +10,23 @@ describe("trimNonEmpty", () => {
expect(trimNonEmpty(null)).toBeUndefined();
});
});
describe("stripUtf8Bom", () => {
it("removes a leading BOM character", () => {
expect(stripUtf8Bom("\uFEFF---\nname: foo\n---\n")).toBe(
"---\nname: foo\n---\n",
);
});
it("leaves text without a BOM unchanged", () => {
expect(stripUtf8Bom("---\nname: foo\n---\n")).toBe("---\nname: foo\n---\n");
});
it("only strips a BOM at the start of the string", () => {
expect(stripUtf8Bom("a\uFEFFb")).toBe("a\uFEFFb");
});
it("handles empty strings", () => {
expect(stripUtf8Bom("")).toBe("");
});
});
+19
View File
@@ -34,3 +34,22 @@ export function maskSecret(value: string): string {
}
return `${value.slice(0, 4)}...${value.slice(-4)}`;
}
/**
* Strip a leading UTF-8 byte order mark (BOM, U+FEFF) from decoded text.
*
* Text editors on Windows (e.g. Notepad's "UTF-8" encoding option) prepend this mark to
* signal the encoding, but `fs.readFileSync(path, "utf8")` does not strip it, so it survives
* into the decoded string as a leading `\uFEFF` character. Frontmatter parsers anchor on
* `^---` and silently fail to match when that character is present, hiding the file's
* name/description (see cline/cline#12151).
*
* We only need to check for this one mark: a BOM disambiguates byte order for multi-byte
* code units (UTF-16, UTF-32), but UTF-8 is a byte-oriented encoding with no byte-order
* ambiguity to resolve, so it has exactly one BOM encoding (`EF BB BF`, i.e. U+FEFF) rather
* than a family of them. Every caller of this helper already reads its input as `utf8`, so a
* file actually encoded as UTF-16/32 would be mis-decoded well before reaching here.
*/
export function stripUtf8Bom(text: string): string {
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}