@evanovation/open-cursor 2.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +270 -0
  3. package/dist/cli/discover.js +527 -0
  4. package/dist/cli/mcptool.js +10339 -0
  5. package/dist/cli/opencode-cursor.js +2989 -0
  6. package/dist/index.js +20588 -0
  7. package/dist/plugin-entry.js +19848 -0
  8. package/package.json +82 -0
  9. package/scripts/cursor-agent-runner.mjs +272 -0
  10. package/scripts/sdk-runner.mjs +412 -0
  11. package/src/acp/metrics.ts +83 -0
  12. package/src/acp/sessions.ts +107 -0
  13. package/src/acp/tools.ts +209 -0
  14. package/src/auth.ts +175 -0
  15. package/src/cli/discover.ts +53 -0
  16. package/src/cli/mcptool.ts +133 -0
  17. package/src/cli/model-discovery.ts +71 -0
  18. package/src/cli/opencode-cursor.ts +1195 -0
  19. package/src/client/cursor-agent-child.ts +459 -0
  20. package/src/client/sdk-child.ts +550 -0
  21. package/src/client/simple.ts +293 -0
  22. package/src/commands/status.ts +39 -0
  23. package/src/index.ts +39 -0
  24. package/src/mcp/client-manager.ts +166 -0
  25. package/src/mcp/config.ts +169 -0
  26. package/src/mcp/tool-bridge.ts +133 -0
  27. package/src/models/config.ts +64 -0
  28. package/src/models/discovery.ts +105 -0
  29. package/src/models/index.ts +3 -0
  30. package/src/models/pricing.ts +196 -0
  31. package/src/models/sync.ts +247 -0
  32. package/src/models/types.ts +11 -0
  33. package/src/models/variants.ts +446 -0
  34. package/src/plugin-entry.ts +28 -0
  35. package/src/plugin-toggle.ts +81 -0
  36. package/src/plugin.ts +2802 -0
  37. package/src/provider/backend.ts +71 -0
  38. package/src/provider/boundary.ts +168 -0
  39. package/src/provider/passthrough-tracker.ts +38 -0
  40. package/src/provider/runtime-interception.ts +818 -0
  41. package/src/provider/tool-loop-guard.ts +644 -0
  42. package/src/provider/tool-schema-compat.ts +800 -0
  43. package/src/provider.ts +268 -0
  44. package/src/proxy/formatter.ts +60 -0
  45. package/src/proxy/handler.ts +29 -0
  46. package/src/proxy/incremental-prompt.ts +74 -0
  47. package/src/proxy/prompt-builder.ts +204 -0
  48. package/src/proxy/server.ts +207 -0
  49. package/src/proxy/session-resume.ts +312 -0
  50. package/src/proxy/tool-loop.ts +359 -0
  51. package/src/proxy/types.ts +13 -0
  52. package/src/services/toast-service.ts +81 -0
  53. package/src/streaming/ai-sdk-parts.ts +109 -0
  54. package/src/streaming/delta-tracker.ts +89 -0
  55. package/src/streaming/line-buffer.ts +44 -0
  56. package/src/streaming/openai-sse.ts +118 -0
  57. package/src/streaming/parser.ts +22 -0
  58. package/src/streaming/types.ts +158 -0
  59. package/src/tools/core/executor.ts +25 -0
  60. package/src/tools/core/registry.ts +27 -0
  61. package/src/tools/core/types.ts +31 -0
  62. package/src/tools/defaults.ts +954 -0
  63. package/src/tools/discovery.ts +140 -0
  64. package/src/tools/executors/cli.ts +59 -0
  65. package/src/tools/executors/local.ts +25 -0
  66. package/src/tools/executors/mcp.ts +39 -0
  67. package/src/tools/executors/sdk.ts +39 -0
  68. package/src/tools/index.ts +8 -0
  69. package/src/tools/registry.ts +34 -0
  70. package/src/tools/router.ts +123 -0
  71. package/src/tools/schema.ts +58 -0
  72. package/src/tools/skills/loader.ts +61 -0
  73. package/src/tools/skills/resolver.ts +21 -0
  74. package/src/tools/types.ts +29 -0
  75. package/src/types.ts +8 -0
  76. package/src/usage.ts +112 -0
  77. package/src/utils/binary.ts +71 -0
  78. package/src/utils/errors.ts +224 -0
  79. package/src/utils/logger.ts +191 -0
  80. package/src/utils/perf.ts +76 -0
@@ -0,0 +1,133 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { createLogger } from "../utils/logger.js";
3
+ import type { McpClientManager } from "./client-manager.js";
4
+
5
+ const log = createLogger("mcp:tool-bridge");
6
+
7
+ export const MCP_TOOL_PREFIX = "mcp__";
8
+
9
+ interface DiscoveredMcpTool {
10
+ name: string;
11
+ serverName: string;
12
+ description?: string;
13
+ inputSchema?: Record<string, unknown>;
14
+ }
15
+
16
+ /**
17
+ * Build plugin `tool()` hook entries for discovered MCP tools.
18
+ *
19
+ * Each MCP tool is namespaced as `mcp__<server_name>__<tool_name>`
20
+ * to avoid collision with local tools and to make the source clear.
21
+ */
22
+ export function buildMcpToolHookEntries(
23
+ tools: DiscoveredMcpTool[],
24
+ manager: McpClientManager,
25
+ ): Record<string, any> {
26
+ const z = tool.schema;
27
+ const entries: Record<string, any> = {};
28
+
29
+ for (const t of tools) {
30
+ const hookName = namespaceMcpTool(t.serverName, t.name);
31
+
32
+ if (entries[hookName]) {
33
+ log.debug("Duplicate MCP tool name, skipping", { hookName });
34
+ continue;
35
+ }
36
+
37
+ const zodArgs = mcpSchemaToZod(t.inputSchema, z);
38
+ const serverName = t.serverName;
39
+ const toolName = t.name;
40
+
41
+ entries[hookName] = tool({
42
+ description: t.description || `MCP tool: ${t.name} (server: ${t.serverName})`,
43
+ args: zodArgs,
44
+ async execute(args: any) {
45
+ log.debug("Executing MCP tool", { server: serverName, tool: toolName });
46
+ const result = await manager.callTool(serverName, toolName, args ?? {});
47
+ if (result.startsWith("Error:")) {
48
+ throw new Error(result);
49
+ }
50
+ return result;
51
+ },
52
+ });
53
+ }
54
+
55
+ log.debug("Built MCP tool hook entries", { count: Object.keys(entries).length });
56
+ return entries;
57
+ }
58
+
59
+ /**
60
+ * Build OpenAI-format tool definitions for discovered MCP tools.
61
+ * These are injected into chat.params so the model sees the tools.
62
+ */
63
+ export function buildMcpToolDefinitions(tools: DiscoveredMcpTool[]): any[] {
64
+ const defs: any[] = [];
65
+
66
+ for (const t of tools) {
67
+ const name = namespaceMcpTool(t.serverName, t.name);
68
+ defs.push({
69
+ type: "function",
70
+ function: {
71
+ name,
72
+ description: t.description || `MCP tool: ${t.name} (server: ${t.serverName})`,
73
+ parameters: t.inputSchema ?? { type: "object", properties: {} },
74
+ },
75
+ });
76
+ }
77
+
78
+ return defs;
79
+ }
80
+
81
+ export function namespaceMcpTool(serverName: string, toolName: string): string {
82
+ const sanitizedServer = serverName.replace(/[^a-zA-Z0-9]/g, "_");
83
+ const sanitizedTool = toolName.replace(/[^a-zA-Z0-9]/g, "_");
84
+ return `${MCP_TOOL_PREFIX}${sanitizedServer}__${sanitizedTool}`;
85
+ }
86
+
87
+ function mcpSchemaToZod(inputSchema: Record<string, unknown> | undefined, z: any): any {
88
+ if (!inputSchema || typeof inputSchema !== "object") {
89
+ return {};
90
+ }
91
+
92
+ const properties = (inputSchema.properties ?? {}) as Record<string, any>;
93
+ const required = (inputSchema.required ?? []) as string[];
94
+ const shape: any = {};
95
+
96
+ for (const [key, prop] of Object.entries(properties)) {
97
+ let zodType: any;
98
+
99
+ switch (prop?.type) {
100
+ case "string":
101
+ zodType = z.string();
102
+ break;
103
+ case "number":
104
+ case "integer":
105
+ zodType = z.number();
106
+ break;
107
+ case "boolean":
108
+ zodType = z.boolean();
109
+ break;
110
+ case "array":
111
+ zodType = z.array(z.any());
112
+ break;
113
+ case "object":
114
+ zodType = z.record(z.string(), z.any());
115
+ break;
116
+ default:
117
+ zodType = z.any();
118
+ break;
119
+ }
120
+
121
+ if (prop?.description) {
122
+ zodType = zodType.describe(prop.description);
123
+ }
124
+
125
+ if (!required.includes(key)) {
126
+ zodType = zodType.optional();
127
+ }
128
+
129
+ shape[key] = zodType;
130
+ }
131
+
132
+ return shape;
133
+ }
@@ -0,0 +1,64 @@
1
+ import type { ModelInfo } from "./types.js";
2
+
3
+ interface OpenCodeModelConfig {
4
+ name: string;
5
+ tools?: boolean;
6
+ reasoning?: boolean;
7
+ description?: string;
8
+ [key: string]: any;
9
+ }
10
+
11
+ interface OpenCodeProviderConfig {
12
+ npm?: string;
13
+ name?: string;
14
+ options?: Record<string, any>;
15
+ models: Record<string, OpenCodeModelConfig>;
16
+ }
17
+
18
+ export class ConfigUpdater {
19
+ formatModels(models: ModelInfo[]): Record<string, OpenCodeModelConfig> {
20
+ const formatted: Record<string, OpenCodeModelConfig> = {};
21
+
22
+ for (const model of models) {
23
+ // Normalize ID for JSON key (replace dots/dashes)
24
+ const key = model.id.replace(/[.-]/g, "");
25
+
26
+ formatted[key] = {
27
+ name: model.name,
28
+ tools: true,
29
+ reasoning: true,
30
+ description: model.description
31
+ };
32
+ }
33
+
34
+ return formatted;
35
+ }
36
+
37
+ mergeModels(
38
+ existing: Record<string, OpenCodeModelConfig>,
39
+ discovered: ModelInfo[]
40
+ ): Record<string, OpenCodeModelConfig> {
41
+ const formatted = this.formatModels(discovered);
42
+
43
+ // Merge, preserving existing custom fields
44
+ return {
45
+ ...formatted,
46
+ ...existing // Existing takes precedence for conflicts
47
+ };
48
+ }
49
+
50
+ generateProviderConfig(
51
+ models: ModelInfo[],
52
+ baseURL: string
53
+ ): OpenCodeProviderConfig {
54
+ return {
55
+ npm: "@ai-sdk/openai-compatible",
56
+ name: "Cursor Agent Provider",
57
+ options: {
58
+ baseURL,
59
+ apiKey: "cursor-agent"
60
+ },
61
+ models: this.formatModels(models)
62
+ };
63
+ }
64
+ }
@@ -0,0 +1,105 @@
1
+ import type { ModelInfo, DiscoveryConfig } from "./types.js";
2
+ import { listModelsViaRunner } from "../client/sdk-child.js";
3
+ import { discoverModelsFromCursorAgent } from "../cli/model-discovery.js";
4
+ import { resolveSdkApiKey } from "../auth.js";
5
+ import { parseCursorBackendPreference } from "../provider/backend.js";
6
+
7
+ interface CacheEntry {
8
+ models: ModelInfo[];
9
+ timestamp: number;
10
+ }
11
+
12
+ export class ModelDiscoveryService {
13
+ private cache: CacheEntry | null = null;
14
+ private cacheTTL: number;
15
+ private fallbackModels: ModelInfo[];
16
+
17
+ constructor(config: DiscoveryConfig = {}) {
18
+ this.cacheTTL = config.cacheTTL || 5 * 60 * 1000; // 5 minutes
19
+ this.fallbackModels = config.fallbackModels || this.getDefaultModels();
20
+ }
21
+
22
+ async discover(apiKey?: string): Promise<ModelInfo[]> {
23
+ // Check cache
24
+ if (this.cache && Date.now() - this.cache.timestamp < this.cacheTTL) {
25
+ return this.cache.models;
26
+ }
27
+
28
+ const backendPreference = parseCursorBackendPreference(process.env.CURSOR_ACP_BACKEND).preference;
29
+ const queryOrder = backendPreference === "sdk"
30
+ ? ["sdk"]
31
+ : backendPreference === "cursor-agent"
32
+ ? ["cursor-agent"]
33
+ : ["cursor-agent", "sdk"];
34
+
35
+ for (const backend of queryOrder) {
36
+ try {
37
+ const models = backend === "cursor-agent"
38
+ ? await this.queryCursorAgent()
39
+ : await this.queryViaSdk(apiKey);
40
+ this.cache = { models, timestamp: Date.now() };
41
+ return models;
42
+ } catch {
43
+ // Try the next backend before falling back to static defaults.
44
+ }
45
+ }
46
+
47
+ this.cache = { models: this.fallbackModels, timestamp: Date.now() };
48
+ return this.fallbackModels;
49
+ }
50
+
51
+ async queryCursorAgent(apiKey?: string): Promise<ModelInfo[]> {
52
+ void apiKey;
53
+ return discoverModelsFromCursorAgent().map((model) => ({
54
+ id: model.id,
55
+ name: model.name,
56
+ description: `Cursor ${model.name} model`,
57
+ }));
58
+ }
59
+
60
+ private async queryViaSdk(apiKey?: string): Promise<ModelInfo[]> {
61
+ // Use the SDK runner to list models
62
+ const key = resolveSdkApiKey({ env: process.env, storedApiKey: apiKey });
63
+ if (!key) {
64
+ throw new Error("No Cursor API key available");
65
+ }
66
+
67
+ const sdkModels = await listModelsViaRunner(key);
68
+
69
+ // Map SDK model format to ModelInfo
70
+ return sdkModels.map((m) => ({
71
+ id: m.id,
72
+ name: m.name,
73
+ description: `Cursor ${m.name} model`,
74
+ }));
75
+ }
76
+
77
+ private getDefaultModels(): ModelInfo[] {
78
+ return [
79
+ { id: "auto", name: "Auto", description: "Auto-select best model" },
80
+ { id: "composer-1.5", name: "Composer 1.5" },
81
+ { id: "composer-1", name: "Composer 1" },
82
+ { id: "opus-4.6-thinking", name: "Claude 4.6 Opus (Thinking)" },
83
+ { id: "opus-4.6", name: "Claude 4.6 Opus" },
84
+ { id: "sonnet-4.6", name: "Claude 4.6 Sonnet" },
85
+ { id: "sonnet-4.6-thinking", name: "Claude 4.6 Sonnet (Thinking)" },
86
+ { id: "opus-4.5", name: "Claude 4.5 Opus" },
87
+ { id: "opus-4.5-thinking", name: "Claude 4.5 Opus (Thinking)" },
88
+ { id: "sonnet-4.5", name: "Claude 4.5 Sonnet" },
89
+ { id: "sonnet-4.5-thinking", name: "Claude 4.5 Sonnet (Thinking)" },
90
+ { id: "gpt-5.4-high", name: "GPT-5.4 High" },
91
+ { id: "gpt-5.4-medium", name: "GPT-5.4" },
92
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
93
+ { id: "gpt-5.2", name: "GPT-5.2" },
94
+ { id: "gemini-3.1-pro", name: "Gemini 3.1 Pro" },
95
+ { id: "gemini-3-pro", name: "Gemini 3 Pro" },
96
+ { id: "gemini-3-flash", name: "Gemini 3 Flash" },
97
+ { id: "grok", name: "Grok" },
98
+ { id: "kimi-k2.5", name: "Kimi K2.5" },
99
+ ];
100
+ }
101
+
102
+ invalidateCache(): void {
103
+ this.cache = null;
104
+ }
105
+ }
@@ -0,0 +1,3 @@
1
+ export { ModelDiscoveryService } from "./discovery.js";
2
+ export { ConfigUpdater } from "./config.js";
3
+ export type { ModelInfo, DiscoveryConfig } from "./types.js";
@@ -0,0 +1,196 @@
1
+ export type OpenCodeModelCost = {
2
+ input: number;
3
+ output: number;
4
+ cache_read?: number;
5
+ cache_write?: number;
6
+ context_over_200k?: OpenCodeModelCost;
7
+ };
8
+
9
+ export type CursorPricingCoverage = {
10
+ priced: string[];
11
+ missing: string[];
12
+ };
13
+
14
+ export type OpenCodeModelCostValidation = {
15
+ valid: boolean;
16
+ errors: string[];
17
+ };
18
+
19
+ export const CURSOR_PRICING_DOC_URL = "https://cursor.com/docs/models-and-pricing";
20
+
21
+ export const CURSOR_PRICING_DOC_MARKERS = [
22
+ "Auto",
23
+ "Composer 2",
24
+ "Composer 1.5",
25
+ "Claude 4.6",
26
+ "Claude 4.6 Sonnet",
27
+ "Claude Opus 4.7",
28
+ "Gemini 3.1 Pro",
29
+ "Gemini 3 Flash",
30
+ "GPT-5.3 Codex",
31
+ "GPT-5.4",
32
+ "GPT-5.5",
33
+ "Grok 4.20",
34
+ "Kimi K2.5",
35
+ ];
36
+
37
+ // Official Cursor prices per 1M tokens from https://cursor.com/docs/models-and-pricing.
38
+ const AUTO_COST = cost(1.25, 6, 0.25, 1.25);
39
+ const COMPOSER_2_COST = cost(0.5, 2.5, 0.2, 0.5);
40
+ const COMPOSER_2_FAST_COST = cost(1.5, 7.5, 0.35, 1.5);
41
+ const COMPOSER_1_5_COST = cost(3.5, 17.5, 0.35, 3.5);
42
+
43
+ const CLAUDE_SONNET_COST = cost(3, 15, 0.3, 3.75);
44
+ const CLAUDE_SONNET_LONG_CONTEXT_COST = cost(6, 22.5, 0.6, 7.5);
45
+ const CLAUDE_SONNET_WITH_LONG_CONTEXT_COST = withLongContext(
46
+ CLAUDE_SONNET_COST,
47
+ CLAUDE_SONNET_LONG_CONTEXT_COST,
48
+ );
49
+ const CLAUDE_OPUS_COST = cost(5, 25, 0.5, 6.25);
50
+ const CLAUDE_OPUS_FAST_COST = cost(30, 150, 3, 37.5);
51
+
52
+ const GEMINI_3_PRO_COST = withLongContext(cost(2, 12, 0.2, 2), cost(4, 18, 0.4, 4));
53
+ const GEMINI_3_FLASH_COST = cost(0.5, 3, 0.05, 0.5);
54
+
55
+ const GPT_5_1_COST = cost(1.25, 10, 0.125, 1.25);
56
+ const GPT_5_2_COST = cost(1.75, 14, 0.175, 1.75);
57
+ const GPT_5_3_CODEX_COST = cost(1.75, 14, 0.175, 1.75);
58
+ const GPT_5_4_COST = withLongContext(cost(2.5, 15, 0.25, 2.5), cost(5, 22.5, 0.5, 5));
59
+ const GPT_5_4_FAST_COST = cost(5, 30, 0.5, 5);
60
+ const GPT_5_4_MINI_COST = cost(0.75, 4.5, 0.075, 0.75);
61
+ const GPT_5_4_NANO_COST = cost(0.2, 1.25, 0.02, 0.2);
62
+ const GPT_5_5_COST = withLongContext(cost(5, 30, 0.5, 5), cost(10, 45, 1, 10));
63
+ const GPT_5_MINI_COST = cost(0.25, 2, 0.025, 0.25);
64
+
65
+ const GROK_4_20_COST = withLongContext(cost(2, 6, 0.2, 2), cost(4, 12, 0.4, 4));
66
+ const KIMI_K2_5_COST = cost(0.6, 3, 0.1, 0.6);
67
+
68
+ export function getCursorModelCost(modelId: string): OpenCodeModelCost | undefined {
69
+ if (modelId === "auto") return AUTO_COST;
70
+ if (modelId === "composer-2-fast") return COMPOSER_2_FAST_COST;
71
+ if (modelId === "composer-2") return COMPOSER_2_COST;
72
+ if (modelId === "composer-1.5") return COMPOSER_1_5_COST;
73
+
74
+ if (modelId.startsWith("claude-opus-4-7")) return CLAUDE_OPUS_COST;
75
+ if (modelId.startsWith("claude-4.6-opus")) {
76
+ return modelId.endsWith("-fast") ? CLAUDE_OPUS_FAST_COST : CLAUDE_OPUS_COST;
77
+ }
78
+ if (modelId.startsWith("claude-4.5-opus")) return CLAUDE_OPUS_COST;
79
+ if (modelId.startsWith("claude-4.6-sonnet")) return CLAUDE_SONNET_WITH_LONG_CONTEXT_COST;
80
+ if (modelId.startsWith("claude-4.5-sonnet")) return CLAUDE_SONNET_WITH_LONG_CONTEXT_COST;
81
+ if (modelId.startsWith("claude-4-sonnet")) return CLAUDE_SONNET_COST;
82
+
83
+ if (modelId === "gemini-3.1-pro") return GEMINI_3_PRO_COST;
84
+ if (modelId === "gemini-3-flash") return GEMINI_3_FLASH_COST;
85
+
86
+ if (modelId.startsWith("gpt-5.5")) return GPT_5_5_COST;
87
+ if (modelId.startsWith("gpt-5.4-mini")) return GPT_5_4_MINI_COST;
88
+ if (modelId.startsWith("gpt-5.4-nano")) return GPT_5_4_NANO_COST;
89
+ if (modelId.startsWith("gpt-5.4")) {
90
+ return modelId.endsWith("-fast") ? GPT_5_4_FAST_COST : GPT_5_4_COST;
91
+ }
92
+ if (modelId.startsWith("gpt-5.3-codex")) return GPT_5_3_CODEX_COST;
93
+ if (modelId.startsWith("gpt-5.2-codex")) return GPT_5_2_COST;
94
+ if (modelId.startsWith("gpt-5.2")) return GPT_5_2_COST;
95
+ if (modelId.startsWith("gpt-5.1-codex-mini")) return GPT_5_MINI_COST;
96
+ if (modelId.startsWith("gpt-5.1-codex-max")) return GPT_5_1_COST;
97
+ if (modelId.startsWith("gpt-5.1")) return GPT_5_1_COST;
98
+ if (modelId === "gpt-5-mini") return GPT_5_MINI_COST;
99
+
100
+ if (modelId.startsWith("grok-4-20")) return GROK_4_20_COST;
101
+ if (modelId === "kimi-k2.5") return KIMI_K2_5_COST;
102
+
103
+ return undefined;
104
+ }
105
+
106
+ export function applyCursorModelCost<T extends Record<string, unknown>>(
107
+ modelId: string,
108
+ entry: T,
109
+ ): T & { cost?: OpenCodeModelCost } {
110
+ const modelCost = getCursorModelCost(modelId);
111
+ if (!modelCost) return entry;
112
+ return { ...entry, cost: modelCost };
113
+ }
114
+
115
+ export function checkCursorPricingCoverage(modelIds: string[]): CursorPricingCoverage {
116
+ const priced: string[] = [];
117
+ const missing: string[] = [];
118
+
119
+ for (const modelId of modelIds) {
120
+ if (getCursorModelCost(modelId)) {
121
+ priced.push(modelId);
122
+ } else {
123
+ missing.push(modelId);
124
+ }
125
+ }
126
+
127
+ return { priced, missing };
128
+ }
129
+
130
+ export function validateOpenCodeModelCost(
131
+ value: unknown,
132
+ path = "cost",
133
+ ): OpenCodeModelCostValidation {
134
+ const errors: string[] = [];
135
+ collectCostValidationErrors(value, path, errors);
136
+ return { valid: errors.length === 0, errors };
137
+ }
138
+
139
+ export function isOpenCodeModelCost(value: unknown): value is OpenCodeModelCost {
140
+ return validateOpenCodeModelCost(value).valid;
141
+ }
142
+
143
+ function cost(input: number, output: number, cacheRead: number, cacheWrite: number): OpenCodeModelCost {
144
+ return {
145
+ input,
146
+ output,
147
+ cache_read: cacheRead,
148
+ cache_write: cacheWrite,
149
+ };
150
+ }
151
+
152
+ function withLongContext(
153
+ base: OpenCodeModelCost,
154
+ longContext: OpenCodeModelCost,
155
+ ): OpenCodeModelCost {
156
+ return {
157
+ ...base,
158
+ context_over_200k: longContext,
159
+ };
160
+ }
161
+
162
+ function collectCostValidationErrors(value: unknown, path: string, errors: string[]): void {
163
+ if (!isRecord(value)) {
164
+ errors.push(`${path} must be an object`);
165
+ return;
166
+ }
167
+
168
+ validateRequiredPrice(value.input, `${path}.input`, errors);
169
+ validateRequiredPrice(value.output, `${path}.output`, errors);
170
+ validateOptionalPrice(value.cache_read, `${path}.cache_read`, errors);
171
+ validateOptionalPrice(value.cache_write, `${path}.cache_write`, errors);
172
+
173
+ if (value.context_over_200k !== undefined) {
174
+ collectCostValidationErrors(value.context_over_200k, `${path}.context_over_200k`, errors);
175
+ }
176
+ }
177
+
178
+ function validateRequiredPrice(value: unknown, path: string, errors: string[]): void {
179
+ if (!isNonNegativeFiniteNumber(value)) {
180
+ errors.push(`${path} must be a non-negative finite number`);
181
+ }
182
+ }
183
+
184
+ function validateOptionalPrice(value: unknown, path: string, errors: string[]): void {
185
+ if (value !== undefined && !isNonNegativeFiniteNumber(value)) {
186
+ errors.push(`${path} must be a non-negative finite number`);
187
+ }
188
+ }
189
+
190
+ function isNonNegativeFiniteNumber(value: unknown): value is number {
191
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
192
+ }
193
+
194
+ function isRecord(value: unknown): value is Record<string, unknown> {
195
+ return typeof value === "object" && value !== null && !Array.isArray(value);
196
+ }