diff --git a/package-lock.json b/package-lock.json index 4430c93..7a7f88f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -226,7 +226,6 @@ "integrity": "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", @@ -379,7 +378,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -706,7 +704,6 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -752,7 +749,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1242,6 +1238,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -1263,6 +1260,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1279,6 +1277,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -1293,6 +1292,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -4443,7 +4443,6 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -4853,7 +4852,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4881,7 +4879,6 @@ "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -4913,7 +4910,6 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -5640,7 +5636,6 @@ "integrity": "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.15.1", "@algolia/client-abtesting": "5.49.1", @@ -6075,7 +6070,6 @@ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -6130,7 +6124,6 @@ "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -6210,7 +6203,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6614,7 +6606,6 @@ "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", @@ -6970,7 +6961,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -7019,7 +7011,6 @@ "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -7453,7 +7444,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7888,7 +7878,6 @@ "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", @@ -8484,6 +8473,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8504,6 +8494,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8693,7 +8684,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -9084,7 +9074,6 @@ "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -11078,6 +11067,7 @@ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -11122,7 +11112,6 @@ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz", "integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==", "license": "MIT", - "peer": true, "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", @@ -11665,7 +11654,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -11763,7 +11751,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11944,6 +11931,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -11961,6 +11949,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -12148,7 +12137,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12161,7 +12149,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12450,6 +12437,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -12462,7 +12450,8 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.12", @@ -12470,6 +12459,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -12482,6 +12472,7 @@ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -12503,6 +12494,7 @@ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -13484,6 +13476,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -14249,7 +14242,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14503,7 +14495,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -16645,7 +16636,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -16761,7 +16751,6 @@ "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", diff --git a/src/server/routes/proxy/geminiCliCompat.thoughtSignature.test.ts b/src/server/routes/proxy/geminiCliCompat.thoughtSignature.test.ts new file mode 100644 index 0000000..5fe466a --- /dev/null +++ b/src/server/routes/proxy/geminiCliCompat.thoughtSignature.test.ts @@ -0,0 +1,196 @@ +import { describe, it, expect } from 'vitest'; +import { buildGeminiGenerateContentRequestFromOpenAi } from './geminiCliCompat.js'; + +describe('thoughtSignature injection in OpenAI→Gemini conversion', () => { + it('injects thoughtSignature from provider_specific_fields into functionCall parts', () => { + const result = buildGeminiGenerateContentRequestFromOpenAi({ + body: { + model: 'gemini-3-flash-preview', + messages: [ + { role: 'user', content: 'What is the weather?' }, + { + role: 'assistant', + content: 'Let me check.', + tool_calls: [ + { + id: 'call_123', + type: 'function', + function: { name: 'get_weather', arguments: '{"city":"Tokyo"}' }, + provider_specific_fields: { thought_signature: 'real_sig_abc' }, + }, + ], + }, + { role: 'tool', tool_call_id: 'call_123', content: '{"temp":"22C"}' }, + ], + }, + modelName: 'gemini-3-flash-preview', + }) as Record; + + const contents = result.contents as Array>; + // Find the model message with functionCall + const modelMsgs = contents.filter((c) => c.role === 'model'); + const fcParts = modelMsgs + .flatMap((m) => (m.parts as Array>)) + .filter((p) => 'functionCall' in p); + + expect(fcParts.length).toBe(1); + expect(fcParts[0].thoughtSignature).toBe('real_sig_abc'); + }); + + it('splits text and signed functionCall parts into separate model messages', () => { + const result = buildGeminiGenerateContentRequestFromOpenAi({ + body: { + model: 'gemini-3-flash-preview', + messages: [ + { role: 'user', content: 'Read the file.' }, + { + role: 'assistant', + content: 'I will read it.', + tool_calls: [ + { + id: 'call_456', + type: 'function', + function: { name: 'Read', arguments: '{"path":"/tmp/x"}' }, + provider_specific_fields: { thought_signature: 'sig_split_test' }, + }, + ], + }, + { role: 'tool', tool_call_id: 'call_456', content: 'file content here' }, + ], + }, + modelName: 'gemini-3-flash-preview', + }) as Record; + + const contents = result.contents as Array>; + const modelMsgs = contents.filter((c) => c.role === 'model'); + + // Should be 2 model messages: text (unsigned) + functionCall (signed) + expect(modelMsgs.length).toBe(2); + + const firstParts = modelMsgs[0].parts as Array>; + const secondParts = modelMsgs[1].parts as Array>; + + // First: text only, no thoughtSignature + expect(firstParts.every((p) => 'text' in p)).toBe(true); + expect(firstParts.every((p) => !('thoughtSignature' in p))).toBe(true); + + // Second: functionCall with thoughtSignature + expect(secondParts.every((p) => 'functionCall' in p)).toBe(true); + expect(secondParts[0].thoughtSignature).toBe('sig_split_test'); + }); + + it('injects dummy sentinel when thinking enabled but no signature available', () => { + const result = buildGeminiGenerateContentRequestFromOpenAi({ + body: { + model: 'gemini-3-flash-preview', + reasoning_effort: 'high', + messages: [ + { role: 'user', content: 'Do something.' }, + { + role: 'assistant', + tool_calls: [ + { + id: 'call_no_sig', + type: 'function', + function: { name: 'Bash', arguments: '{"command":"ls"}' }, + // No provider_specific_fields — signature missing + }, + ], + }, + { role: 'tool', tool_call_id: 'call_no_sig', content: 'file1\nfile2' }, + ], + }, + modelName: 'gemini-3-flash-preview', + }) as Record; + + const contents = result.contents as Array>; + const modelMsgs = contents.filter((c) => c.role === 'model'); + const fcParts = modelMsgs + .flatMap((m) => (m.parts as Array>)) + .filter((p) => 'functionCall' in p); + + expect(fcParts.length).toBe(1); + // Should have a dummy sentinel, not be missing + expect(typeof fcParts[0].thoughtSignature).toBe('string'); + expect((fcParts[0].thoughtSignature as string).length).toBeGreaterThan(0); + }); + + it('does not inject thoughtSignature when thinking is not enabled', () => { + const result = buildGeminiGenerateContentRequestFromOpenAi({ + body: { + model: 'gemini-2.5-flash', + messages: [ + { role: 'user', content: 'Hello' }, + { + role: 'assistant', + tool_calls: [ + { + id: 'call_no_think', + type: 'function', + function: { name: 'Read', arguments: '{"path":"/x"}' }, + }, + ], + }, + { role: 'tool', tool_call_id: 'call_no_think', content: 'data' }, + ], + }, + modelName: 'gemini-2.5-flash', + }) as Record; + + const contents = result.contents as Array>; + const modelMsgs = contents.filter((c) => c.role === 'model'); + const fcParts = modelMsgs + .flatMap((m) => (m.parts as Array>)) + .filter((p) => 'functionCall' in p); + + expect(fcParts.length).toBe(1); + // No thinking → no signature injected + expect(fcParts[0].thoughtSignature).toBeUndefined(); + }); + + it('preserves functionResponse count matching functionCall count', () => { + const result = buildGeminiGenerateContentRequestFromOpenAi({ + body: { + model: 'gemini-3-flash-preview', + messages: [ + { role: 'user', content: 'Read two files.' }, + { + role: 'assistant', + tool_calls: [ + { + id: 'call_a', + type: 'function', + function: { name: 'Read', arguments: '{"path":"/a"}' }, + provider_specific_fields: { thought_signature: 'sig_a' }, + }, + { + id: 'call_b', + type: 'function', + function: { name: 'Read', arguments: '{"path":"/b"}' }, + provider_specific_fields: { thought_signature: 'sig_b' }, + }, + ], + }, + { role: 'tool', tool_call_id: 'call_a', content: 'content a' }, + { role: 'tool', tool_call_id: 'call_b', content: 'content b' }, + ], + }, + modelName: 'gemini-3-flash-preview', + }) as Record; + + const contents = result.contents as Array>; + + // Count functionCall and functionResponse parts + let fcCount = 0; + let frCount = 0; + for (const content of contents) { + for (const part of (content.parts as Array>)) { + if ('functionCall' in part) fcCount++; + if ('functionResponse' in part) frCount++; + } + } + + expect(fcCount).toBe(2); + expect(frCount).toBe(2); + }); +});