@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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.
- package/README.md +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -5,628 +5,636 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, it, expect, vi } from "vitest";
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
transformRequestBody,
|
|
9
|
+
validateBodyType,
|
|
10
|
+
cloneBodyForRetry,
|
|
11
|
+
detectDoublePrefix,
|
|
12
|
+
extractToolNamesFromBody,
|
|
13
13
|
} from "./body.js";
|
|
14
14
|
import type { RuntimeContext, SignatureConfig } from "../types.js";
|
|
15
15
|
|
|
16
16
|
const mockRuntime: RuntimeContext = {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
persistentUserId: "user-123",
|
|
18
|
+
accountId: "acc-456",
|
|
19
|
+
sessionId: "sess-789",
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const mockSignature: SignatureConfig = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
enabled: true,
|
|
24
|
+
claudeCliVersion: "0.2.45",
|
|
25
|
+
promptCompactionMode: "minimal",
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
describe("transformRequestBody - type validation", () => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
29
|
+
it("should reject undefined body without error", () => {
|
|
30
|
+
const result = transformRequestBody(undefined, mockSignature, mockRuntime);
|
|
31
|
+
expect(result).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should reject null body without error", () => {
|
|
35
|
+
const result = transformRequestBody(null as unknown as string, mockSignature, mockRuntime);
|
|
36
|
+
expect(result).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should reject non-string body with clear error", () => {
|
|
40
|
+
const invalidBodies = [123, {}, [], true, () => {}];
|
|
41
|
+
|
|
42
|
+
for (const body of invalidBodies) {
|
|
43
|
+
expect(() => transformRequestBody(body as unknown as string, mockSignature, mockRuntime)).toThrow(
|
|
44
|
+
/opencode-anthropic-auth: expected string body, got /,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should validate body type at runtime with descriptive error", () => {
|
|
50
|
+
const debugLog = vi.fn();
|
|
51
|
+
const body = { not: "a string" };
|
|
52
|
+
|
|
53
|
+
expect(() =>
|
|
54
|
+
transformRequestBody(body as unknown as string, mockSignature, mockRuntime, true, debugLog),
|
|
55
|
+
).toThrow(
|
|
56
|
+
"opencode-anthropic-auth: expected string body, got object. This plugin does not support stream bodies. Please file a bug with the OpenCode version.",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
describe("transformRequestBody - double-prefix defense", () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
it("should detect double-prefix in tool_use blocks", () => {
|
|
76
|
-
const body = JSON.stringify({
|
|
77
|
-
model: "claude-sonnet-4-20250514",
|
|
78
|
-
messages: [
|
|
79
|
-
{
|
|
80
|
-
role: "assistant",
|
|
81
|
-
content: [{ type: "tool_use", name: "mcp_mcp_read_file", input: {} }],
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
expect(() => transformRequestBody(body, mockSignature, mockRuntime)).toThrow(
|
|
87
|
-
/Double tool prefix detected in tool_use block/,
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("should double-prefix literal mcp_ tool definitions to preserve round-trip names", () => {
|
|
92
|
-
const body = JSON.stringify({
|
|
93
|
-
model: "claude-sonnet-4-20250514",
|
|
94
|
-
messages: [{ role: "user", content: "test" }],
|
|
95
|
-
tools: [{ name: "mcp_read_file", description: "Read a file" }],
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
99
|
-
const parsed = JSON.parse(result!);
|
|
100
|
-
|
|
101
|
-
expect(parsed.tools[0].name).toBe("mcp_mcp_read_file");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("should keep literal mcp_ tool definitions round-trip safe", () => {
|
|
105
|
-
const body = JSON.stringify({
|
|
106
|
-
model: "claude-sonnet-4-20250514",
|
|
107
|
-
tools: [
|
|
108
|
-
{ name: "mcp_server1__tool1", description: "Tool 1" },
|
|
109
|
-
{ name: "mcp_server2__tool2", description: "Tool 2" },
|
|
110
|
-
],
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
114
|
-
const parsed = JSON.parse(result!);
|
|
115
|
-
|
|
116
|
-
expect(parsed.tools[0].name).toBe("mcp_mcp_server1__tool1");
|
|
117
|
-
expect(parsed.tools[1].name).toBe("mcp_mcp_server2__tool2");
|
|
118
|
-
});
|
|
119
|
-
});
|
|
62
|
+
it("should detect and reject double-prefixed tool names (mcp_mcp_)", () => {
|
|
63
|
+
const body = JSON.stringify({
|
|
64
|
+
model: "claude-sonnet-4-20250514",
|
|
65
|
+
messages: [{ role: "user", content: "test" }],
|
|
66
|
+
tools: [
|
|
67
|
+
{ name: "mcp_mcp_read_file", description: "Read a file" },
|
|
68
|
+
{ name: "mcp_mcp_write_file", description: "Write a file" },
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(() => transformRequestBody(body, mockSignature, mockRuntime)).toThrow(
|
|
73
|
+
/Double tool prefix detected: mcp_mcp_/,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
120
76
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
77
|
+
it("should detect double-prefix in tool_use blocks", () => {
|
|
78
|
+
const body = JSON.stringify({
|
|
79
|
+
model: "claude-sonnet-4-20250514",
|
|
80
|
+
messages: [
|
|
81
|
+
{
|
|
82
|
+
role: "assistant",
|
|
83
|
+
content: [{ type: "tool_use", name: "mcp_mcp_read_file", input: {} }],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(() => transformRequestBody(body, mockSignature, mockRuntime)).toThrow(
|
|
89
|
+
/Double tool prefix detected in tool_use block/,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should double-prefix literal mcp_ tool definitions to preserve round-trip names", () => {
|
|
94
|
+
const body = JSON.stringify({
|
|
95
|
+
model: "claude-sonnet-4-20250514",
|
|
96
|
+
messages: [{ role: "user", content: "test" }],
|
|
97
|
+
tools: [{ name: "mcp_read_file", description: "Read a file" }],
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
101
|
+
const parsed = JSON.parse(result!);
|
|
102
|
+
|
|
103
|
+
expect(parsed.tools[0].name).toBe("mcp_mcp_read_file");
|
|
127
104
|
});
|
|
128
105
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
106
|
+
it("should keep literal mcp_ tool definitions round-trip safe", () => {
|
|
107
|
+
const body = JSON.stringify({
|
|
108
|
+
model: "claude-sonnet-4-20250514",
|
|
109
|
+
tools: [
|
|
110
|
+
{ name: "mcp_server1__tool1", description: "Tool 1" },
|
|
111
|
+
{ name: "mcp_server2__tool2", description: "Tool 2" },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
132
114
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
115
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
116
|
+
const parsed = JSON.parse(result!);
|
|
136
117
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
118
|
+
expect(parsed.tools[0].name).toBe("mcp_mcp_server1__tool1");
|
|
119
|
+
expect(parsed.tools[1].name).toBe("mcp_mcp_server2__tool2");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("transformRequestBody - body cloning for retries", () => {
|
|
124
|
+
it("should clone body before transformation to preserve original", () => {
|
|
125
|
+
const originalBody = JSON.stringify({
|
|
126
|
+
model: "claude-sonnet-4-20250514",
|
|
127
|
+
messages: [{ role: "user", content: "test" }],
|
|
128
|
+
tools: [{ name: "read_file", description: "Read a file" }],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result1 = transformRequestBody(originalBody, mockSignature, mockRuntime);
|
|
132
|
+
const result2 = transformRequestBody(originalBody, mockSignature, mockRuntime);
|
|
133
|
+
expect(result1).toBe(result2);
|
|
134
|
+
|
|
135
|
+
const parsedOriginal = JSON.parse(originalBody);
|
|
136
|
+
expect(parsedOriginal.tools[0].name).toBe("read_file");
|
|
137
|
+
});
|
|
140
138
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
model: "claude-sonnet-4-20250514",
|
|
144
|
-
messages: [{ role: "user", content: "test" }],
|
|
139
|
+
it("should return empty bodies unchanged", () => {
|
|
140
|
+
expect(transformRequestBody("", mockSignature, mockRuntime)).toBe("");
|
|
145
141
|
});
|
|
146
142
|
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
it("should handle retry with same body multiple times", () => {
|
|
144
|
+
const body = JSON.stringify({
|
|
145
|
+
model: "claude-sonnet-4-20250514",
|
|
146
|
+
messages: [{ role: "user", content: "test" }],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result1 = transformRequestBody(body, mockSignature, mockRuntime);
|
|
150
|
+
expect(result1).toBeDefined();
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
const result2 = transformRequestBody(body, mockSignature, mockRuntime);
|
|
153
|
+
expect(result2).toBeDefined();
|
|
154
|
+
expect(result1).toBe(result2);
|
|
155
|
+
});
|
|
154
156
|
});
|
|
155
157
|
|
|
156
158
|
describe("transformRequestBody - tool name handling", () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
159
|
+
it("should add mcp_ prefix to unprefixed tool names", () => {
|
|
160
|
+
const body = JSON.stringify({
|
|
161
|
+
model: "claude-sonnet-4-20250514",
|
|
162
|
+
tools: [
|
|
163
|
+
{ name: "read_file", description: "Read a file" },
|
|
164
|
+
{ name: "write_file", description: "Write a file" },
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
169
|
+
const parsed = JSON.parse(result!);
|
|
170
|
+
|
|
171
|
+
expect(parsed.tools[0].name).toBe("mcp_read_file");
|
|
172
|
+
expect(parsed.tools[1].name).toBe("mcp_write_file");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should handle historical tool_use.name with prefix correctly", () => {
|
|
176
|
+
const body = JSON.stringify({
|
|
177
|
+
model: "claude-sonnet-4-20250514",
|
|
178
|
+
messages: [
|
|
179
|
+
{
|
|
180
|
+
role: "assistant",
|
|
181
|
+
content: [{ type: "tool_use", name: "mcp_read_file", input: { path: "/test" } }],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
187
|
+
const parsed = JSON.parse(result!);
|
|
188
|
+
|
|
189
|
+
// Should preserve the prefixed name in historical context
|
|
190
|
+
expect(parsed.messages[0].content[0].name).toBe("mcp_read_file");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("should handle mixed prefixed and unprefixed tools", () => {
|
|
194
|
+
const body = JSON.stringify({
|
|
195
|
+
model: "claude-sonnet-4-20250514",
|
|
196
|
+
tools: [
|
|
197
|
+
{ name: "read_file", description: "Read" },
|
|
198
|
+
{ name: "mcp_existing_tool", description: "Existing" },
|
|
199
|
+
{ name: "write_file", description: "Write" },
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
204
|
+
const parsed = JSON.parse(result!);
|
|
205
|
+
|
|
206
|
+
expect(parsed.tools[0].name).toBe("mcp_read_file");
|
|
207
|
+
expect(parsed.tools[1].name).toBe("mcp_mcp_existing_tool");
|
|
208
|
+
expect(parsed.tools[2].name).toBe("mcp_write_file");
|
|
209
|
+
});
|
|
208
210
|
});
|
|
209
211
|
|
|
210
212
|
describe("transformRequestBody - structure preservation", () => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
],
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
263
|
-
const parsed = JSON.parse(result!);
|
|
264
|
-
|
|
265
|
-
expect(parsed.messages[0].content).toHaveLength(2);
|
|
266
|
-
expect(parsed.messages[0].content[0].type).toBe("text");
|
|
267
|
-
expect(parsed.messages[0].content[1].type).toBe("tool_result");
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it("should preserve nested structures in tool input", () => {
|
|
271
|
-
const body = JSON.stringify({
|
|
272
|
-
model: "claude-sonnet-4-20250514",
|
|
273
|
-
messages: [
|
|
274
|
-
{
|
|
275
|
-
role: "assistant",
|
|
276
|
-
content: [
|
|
277
|
-
{
|
|
278
|
-
type: "tool_use",
|
|
279
|
-
name: "complex_tool",
|
|
280
|
-
input: {
|
|
281
|
-
nested: {
|
|
282
|
-
deep: {
|
|
283
|
-
value: "test",
|
|
284
|
-
array: [1, 2, 3],
|
|
285
|
-
},
|
|
213
|
+
it("should preserve all non-tool fields during transformation", () => {
|
|
214
|
+
const body = JSON.stringify({
|
|
215
|
+
model: "claude-sonnet-4-20250514",
|
|
216
|
+
max_tokens: 4096,
|
|
217
|
+
temperature: 0.7,
|
|
218
|
+
system: [{ type: "text", text: "You are helpful" }],
|
|
219
|
+
messages: [
|
|
220
|
+
{ role: "user", content: "Hello" },
|
|
221
|
+
{ role: "assistant", content: "Hi there" },
|
|
222
|
+
],
|
|
223
|
+
metadata: { user_id: "test-user" },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
227
|
+
const parsed = JSON.parse(result!);
|
|
228
|
+
|
|
229
|
+
expect(parsed.model).toBe("claude-sonnet-4-20250514");
|
|
230
|
+
expect(parsed.max_tokens).toBe(4096);
|
|
231
|
+
expect(parsed.temperature).toBe(0.7);
|
|
232
|
+
// The original "You are helpful" block was relocated to the first user
|
|
233
|
+
// message wrapper. parsed.system now only contains billing + identity.
|
|
234
|
+
expect(parsed.system.some((block: { text?: string }) => block.text === "You are helpful")).toBe(false);
|
|
235
|
+
const firstUserContent = parsed.messages[0].content;
|
|
236
|
+
const wrappedText = typeof firstUserContent === "string" ? firstUserContent : firstUserContent[0].text;
|
|
237
|
+
expect(wrappedText).toContain("<system-instructions>");
|
|
238
|
+
expect(wrappedText).toContain("You are helpful");
|
|
239
|
+
// Original messages are preserved alongside the prepended wrapper text.
|
|
240
|
+
expect(parsed.messages).toHaveLength(2);
|
|
241
|
+
expect(parsed.metadata.user_id).toContain('"device_id":"user-123"');
|
|
242
|
+
expect(parsed.metadata.user_id).toContain('"account_uuid":"acc-456"');
|
|
243
|
+
expect(parsed.metadata.user_id).toContain('"session_id":"sess-789"');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should handle request with body in input correctly", () => {
|
|
247
|
+
const body = JSON.stringify({
|
|
248
|
+
model: "claude-sonnet-4-20250514",
|
|
249
|
+
messages: [
|
|
250
|
+
{
|
|
251
|
+
role: "user",
|
|
252
|
+
content: [
|
|
253
|
+
{ type: "text", text: "Process this" },
|
|
254
|
+
{
|
|
255
|
+
type: "tool_result",
|
|
256
|
+
tool_use_id: "tool_123",
|
|
257
|
+
content: "some result",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
286
260
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
265
|
+
const parsed = JSON.parse(result!);
|
|
266
|
+
|
|
267
|
+
expect(parsed.messages[0].content).toHaveLength(2);
|
|
268
|
+
expect(parsed.messages[0].content[0].type).toBe("text");
|
|
269
|
+
expect(parsed.messages[0].content[1].type).toBe("tool_result");
|
|
292
270
|
});
|
|
293
271
|
|
|
294
|
-
|
|
295
|
-
|
|
272
|
+
it("should preserve nested structures in tool input", () => {
|
|
273
|
+
const body = JSON.stringify({
|
|
274
|
+
model: "claude-sonnet-4-20250514",
|
|
275
|
+
messages: [
|
|
276
|
+
{
|
|
277
|
+
role: "assistant",
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "tool_use",
|
|
281
|
+
name: "complex_tool",
|
|
282
|
+
input: {
|
|
283
|
+
nested: {
|
|
284
|
+
deep: {
|
|
285
|
+
value: "test",
|
|
286
|
+
array: [1, 2, 3],
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
297
|
+
const parsed = JSON.parse(result!);
|
|
296
298
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
const toolUse = parsed.messages[0].content[0];
|
|
300
|
+
expect(toolUse.input.nested.deep.value).toBe("test");
|
|
301
|
+
expect(toolUse.input.nested.deep.array).toEqual([1, 2, 3]);
|
|
302
|
+
});
|
|
301
303
|
});
|
|
302
304
|
|
|
303
305
|
describe("validateBodyType", () => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
306
|
+
it("should return true for valid string body", () => {
|
|
307
|
+
expect(validateBodyType('{"test": true}')).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should return false for undefined", () => {
|
|
311
|
+
expect(validateBodyType(undefined)).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should return false for null", () => {
|
|
315
|
+
expect(validateBodyType(null as unknown as string)).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should return false for non-string types", () => {
|
|
319
|
+
expect(validateBodyType(123 as unknown as string)).toBe(false);
|
|
320
|
+
expect(validateBodyType({} as unknown as string)).toBe(false);
|
|
321
|
+
expect(validateBodyType([] as unknown as string)).toBe(false);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should throw with descriptive message when throwOnInvalid is true", () => {
|
|
325
|
+
expect(() => validateBodyType(123 as unknown as string, true)).toThrow(
|
|
326
|
+
"opencode-anthropic-auth: expected string body, got number. This plugin does not support stream bodies. Please file a bug with the OpenCode version.",
|
|
327
|
+
);
|
|
328
|
+
});
|
|
327
329
|
});
|
|
328
330
|
|
|
329
331
|
describe("cloneBodyForRetry", () => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
332
|
+
it("should return the same string value for retry", () => {
|
|
333
|
+
const original = '{"test": true}';
|
|
334
|
+
const cloned = cloneBodyForRetry(original);
|
|
333
335
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
expect(cloned).toBe(original);
|
|
337
|
+
});
|
|
336
338
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
339
|
+
it("should allow empty string bodies", () => {
|
|
340
|
+
expect(() => cloneBodyForRetry("")).not.toThrow();
|
|
341
|
+
});
|
|
340
342
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
it("should handle empty but valid body", () => {
|
|
344
|
+
expect(() => cloneBodyForRetry("{}")).not.toThrow();
|
|
345
|
+
});
|
|
344
346
|
});
|
|
345
347
|
|
|
346
348
|
describe("detectDoublePrefix", () => {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
349
|
+
it("should detect mcp_mcp_ prefix", () => {
|
|
350
|
+
expect(detectDoublePrefix("mcp_mcp_read_file")).toBe(true);
|
|
351
|
+
});
|
|
350
352
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
353
|
+
it("should not detect single mcp_ prefix", () => {
|
|
354
|
+
expect(detectDoublePrefix("mcp_read_file")).toBe(false);
|
|
355
|
+
});
|
|
354
356
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
it("should not detect unprefixed names", () => {
|
|
358
|
+
expect(detectDoublePrefix("read_file")).toBe(false);
|
|
359
|
+
});
|
|
358
360
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
it("should detect triple prefix", () => {
|
|
362
|
+
expect(detectDoublePrefix("mcp_mcp_mcp_read_file")).toBe(true);
|
|
363
|
+
});
|
|
362
364
|
});
|
|
363
365
|
|
|
364
366
|
describe("extractToolNamesFromBody", () => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
367
|
+
it("should extract tool names from tools array", () => {
|
|
368
|
+
const body = JSON.stringify({
|
|
369
|
+
tools: [{ name: "tool1" }, { name: "tool2" }],
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const names = extractToolNamesFromBody(body);
|
|
373
|
+
expect(names).toEqual(["tool1", "tool2"]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("should extract tool names from tool_use blocks", () => {
|
|
377
|
+
const body = JSON.stringify({
|
|
378
|
+
messages: [
|
|
379
|
+
{
|
|
380
|
+
content: [
|
|
381
|
+
{ type: "tool_use", name: "tool1" },
|
|
382
|
+
{ type: "text", text: "hello" },
|
|
383
|
+
{ type: "tool_use", name: "tool2" },
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const names = extractToolNamesFromBody(body);
|
|
390
|
+
expect(names).toEqual(["tool1", "tool2"]);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("should return empty array for body without tools", () => {
|
|
394
|
+
const body = JSON.stringify({ messages: [] });
|
|
395
|
+
const names = extractToolNamesFromBody(body);
|
|
396
|
+
expect(names).toEqual([]);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("should throw for invalid JSON", () => {
|
|
400
|
+
expect(() => extractToolNamesFromBody("not json")).toThrow();
|
|
401
|
+
});
|
|
400
402
|
});
|
|
401
403
|
|
|
402
404
|
describe("transformRequestBody - aggressive system block relocation", () => {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
405
|
+
it("keeps only billing + identity blocks in parsed.system", () => {
|
|
406
|
+
const body = JSON.stringify({
|
|
407
|
+
model: "claude-sonnet-4-20250514",
|
|
408
|
+
messages: [{ role: "user", content: "hi" }],
|
|
409
|
+
system: [
|
|
410
|
+
{ type: "text", text: "You are a helpful assistant." },
|
|
411
|
+
{ type: "text", text: "Working dir: /Users/vacbo/Documents/Projects/opencode-anthropic-fix" },
|
|
412
|
+
{ type: "text", text: "Plugin: @vacbo/opencode-anthropic-fix v0.1.3" },
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
417
|
+
const parsed = JSON.parse(result!);
|
|
418
|
+
|
|
419
|
+
// System contains exactly 2 blocks: billing header + identity string.
|
|
420
|
+
expect(parsed.system).toHaveLength(2);
|
|
421
|
+
expect(parsed.system[0].text).toMatch(/^x-anthropic-billing-header:/);
|
|
422
|
+
expect(parsed.system[1].text).toBe("You are Claude Code, Anthropic's official CLI for Claude.");
|
|
423
|
+
|
|
424
|
+
// None of the original third-party blocks survived in system.
|
|
425
|
+
const systemTexts = parsed.system.map((b: { text: string }) => b.text);
|
|
426
|
+
expect(systemTexts.some((t: string) => t.includes("helpful assistant"))).toBe(false);
|
|
427
|
+
expect(systemTexts.some((t: string) => t.includes("Working dir:"))).toBe(false);
|
|
428
|
+
expect(systemTexts.some((t: string) => t.includes("Plugin:"))).toBe(false);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("relocates non-CC system blocks into the first user message wrapped in <system-instructions>", () => {
|
|
432
|
+
const body = JSON.stringify({
|
|
433
|
+
model: "claude-sonnet-4-20250514",
|
|
434
|
+
messages: [{ role: "user", content: "what do you know about the codebase?" }],
|
|
435
|
+
system: [
|
|
436
|
+
{ type: "text", text: "You are a helpful assistant." },
|
|
437
|
+
{ type: "text", text: "Working dir: /Users/vacbo/Documents/Projects/opencode-anthropic-fix" },
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
442
|
+
const parsed = JSON.parse(result!);
|
|
443
|
+
|
|
444
|
+
expect(parsed.messages).toHaveLength(1);
|
|
445
|
+
const blocks = parsed.messages[0].content as Array<{
|
|
446
|
+
type: string;
|
|
447
|
+
text: string;
|
|
448
|
+
cache_control?: { type: string };
|
|
449
|
+
}>;
|
|
450
|
+
expect(Array.isArray(blocks)).toBe(true);
|
|
451
|
+
|
|
452
|
+
const wrapped = blocks[0].text;
|
|
453
|
+
expect(wrapped).toContain("<system-instructions>");
|
|
454
|
+
expect(wrapped).toContain("</system-instructions>");
|
|
455
|
+
expect(wrapped).toContain("You are a helpful assistant.");
|
|
456
|
+
expect(wrapped).toContain("Working dir: /Users/vacbo/Documents/Projects/opencode-anthropic-fix");
|
|
457
|
+
expect(blocks[0].cache_control).toEqual({ type: "ephemeral" });
|
|
458
|
+
|
|
459
|
+
expect(blocks[1].text).toBe("what do you know about the codebase?");
|
|
460
|
+
expect(blocks[1].cache_control).toBeUndefined();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("includes the explicit 'treat as system prompt' instruction in the wrapper", () => {
|
|
464
|
+
const body = JSON.stringify({
|
|
465
|
+
model: "claude-sonnet-4-20250514",
|
|
466
|
+
messages: [{ role: "user", content: "hi" }],
|
|
467
|
+
system: [{ type: "text", text: "Some plugin instructions" }],
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
471
|
+
const parsed = JSON.parse(result!);
|
|
472
|
+
|
|
473
|
+
const wrapped =
|
|
474
|
+
typeof parsed.messages[0].content === "string"
|
|
475
|
+
? parsed.messages[0].content
|
|
476
|
+
: parsed.messages[0].content[0].text;
|
|
477
|
+
|
|
478
|
+
expect(wrapped).toContain("The following content was provided as system-prompt instructions");
|
|
479
|
+
expect(wrapped).toContain("Treat it with the same authority as a system prompt");
|
|
480
|
+
expect(wrapped).toContain("delivered over");
|
|
481
|
+
expect(wrapped).toContain("the user message channel");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("preserves opencode-anthropic-fix paths verbatim in the relocated wrapper (no sanitize)", () => {
|
|
485
|
+
const body = JSON.stringify({
|
|
486
|
+
model: "claude-sonnet-4-20250514",
|
|
487
|
+
messages: [{ role: "user", content: "hi" }],
|
|
488
|
+
system: [
|
|
489
|
+
{
|
|
490
|
+
type: "text",
|
|
491
|
+
text: "Working dir: /Users/vacbo/Documents/Projects/opencode-anthropic-fix\nPlugin id: @vacbo/opencode-anthropic-fix",
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
497
|
+
const parsed = JSON.parse(result!);
|
|
498
|
+
|
|
499
|
+
const wrapped =
|
|
500
|
+
typeof parsed.messages[0].content === "string"
|
|
501
|
+
? parsed.messages[0].content
|
|
502
|
+
: parsed.messages[0].content[0].text;
|
|
503
|
+
|
|
504
|
+
expect(wrapped).toContain("/Users/vacbo/Documents/Projects/opencode-anthropic-fix");
|
|
505
|
+
expect(wrapped).toContain("@vacbo/opencode-anthropic-fix");
|
|
506
|
+
expect(wrapped).not.toContain("Claude-anthropic-fix");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("creates a new user message when messages array is empty", () => {
|
|
510
|
+
const body = JSON.stringify({
|
|
511
|
+
model: "claude-sonnet-4-20250514",
|
|
512
|
+
messages: [],
|
|
513
|
+
system: [{ type: "text", text: "Some instructions" }],
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
517
|
+
const parsed = JSON.parse(result!);
|
|
518
|
+
|
|
519
|
+
expect(parsed.messages).toHaveLength(1);
|
|
520
|
+
expect(parsed.messages[0].role).toBe("user");
|
|
521
|
+
const content = parsed.messages[0].content;
|
|
522
|
+
const wrapped = typeof content === "string" ? content : content[0].text;
|
|
523
|
+
expect(wrapped).toContain("Some instructions");
|
|
524
|
+
expect(wrapped).toContain("<system-instructions>");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("prepends a new user message when first message is from assistant", () => {
|
|
528
|
+
const body = JSON.stringify({
|
|
529
|
+
model: "claude-sonnet-4-20250514",
|
|
530
|
+
messages: [
|
|
531
|
+
{ role: "assistant", content: "previous turn" },
|
|
532
|
+
{ role: "user", content: "follow up" },
|
|
533
|
+
],
|
|
534
|
+
system: [{ type: "text", text: "Plugin instructions" }],
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
538
|
+
const parsed = JSON.parse(result!);
|
|
539
|
+
|
|
540
|
+
expect(parsed.messages).toHaveLength(3);
|
|
541
|
+
expect(parsed.messages[0].role).toBe("user");
|
|
542
|
+
const wrapped =
|
|
543
|
+
typeof parsed.messages[0].content === "string"
|
|
544
|
+
? parsed.messages[0].content
|
|
545
|
+
: parsed.messages[0].content[0].text;
|
|
546
|
+
expect(wrapped).toContain("<system-instructions>");
|
|
547
|
+
expect(wrapped).toContain("Plugin instructions");
|
|
548
|
+
// Original turns survive in order.
|
|
549
|
+
expect(parsed.messages[1].role).toBe("assistant");
|
|
550
|
+
expect(parsed.messages[1].content).toBe("previous turn");
|
|
551
|
+
expect(parsed.messages[2].role).toBe("user");
|
|
552
|
+
expect(parsed.messages[2].content).toBe("follow up");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("merges relocated wrapper into the first user message when content is a string", () => {
|
|
556
|
+
const body = JSON.stringify({
|
|
557
|
+
model: "claude-sonnet-4-20250514",
|
|
558
|
+
messages: [{ role: "user", content: "the original user request" }],
|
|
559
|
+
system: [{ type: "text", text: "Plugin instructions" }],
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
563
|
+
const parsed = JSON.parse(result!);
|
|
564
|
+
|
|
565
|
+
expect(parsed.messages).toHaveLength(1);
|
|
566
|
+
expect(Array.isArray(parsed.messages[0].content)).toBe(true);
|
|
567
|
+
const blocks = parsed.messages[0].content as Array<{
|
|
568
|
+
type: string;
|
|
569
|
+
text: string;
|
|
570
|
+
cache_control?: { type: string };
|
|
571
|
+
}>;
|
|
572
|
+
expect(blocks).toHaveLength(2);
|
|
573
|
+
expect(blocks[0].text).toContain("<system-instructions>");
|
|
574
|
+
expect(blocks[0].text).toContain("Plugin instructions");
|
|
575
|
+
expect(blocks[0].cache_control).toEqual({ type: "ephemeral" });
|
|
576
|
+
expect(blocks[1].text).toBe("the original user request");
|
|
577
|
+
expect(blocks[1].cache_control).toBeUndefined();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("merges relocated wrapper into the first user message when content is an array", () => {
|
|
581
|
+
const body = JSON.stringify({
|
|
582
|
+
model: "claude-sonnet-4-20250514",
|
|
583
|
+
messages: [
|
|
584
|
+
{
|
|
585
|
+
role: "user",
|
|
586
|
+
content: [{ type: "text", text: "structured user turn" }],
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
system: [{ type: "text", text: "Plugin instructions" }],
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime);
|
|
593
|
+
const parsed = JSON.parse(result!);
|
|
594
|
+
|
|
595
|
+
expect(parsed.messages).toHaveLength(1);
|
|
596
|
+
expect(Array.isArray(parsed.messages[0].content)).toBe(true);
|
|
597
|
+
const blocks = parsed.messages[0].content as Array<{
|
|
598
|
+
type: string;
|
|
599
|
+
text: string;
|
|
600
|
+
cache_control?: { type: string };
|
|
601
|
+
}>;
|
|
602
|
+
expect(blocks[0].type).toBe("text");
|
|
603
|
+
expect(blocks[0].text).toContain("<system-instructions>");
|
|
604
|
+
expect(blocks[0].text).toContain("Plugin instructions");
|
|
605
|
+
expect(blocks[0].cache_control).toEqual({ type: "ephemeral" });
|
|
606
|
+
expect(blocks[1].text).toBe("structured user turn");
|
|
607
|
+
expect(blocks[1].cache_control).toBeUndefined();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("does not relocate when signature.enabled is false (legacy passthrough)", () => {
|
|
611
|
+
const body = JSON.stringify({
|
|
612
|
+
model: "claude-sonnet-4-20250514",
|
|
613
|
+
messages: [{ role: "user", content: "hi" }],
|
|
614
|
+
system: [{ type: "text", text: "Plugin instructions" }],
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const result = transformRequestBody(body, { ...mockSignature, enabled: false }, mockRuntime);
|
|
618
|
+
const parsed = JSON.parse(result!);
|
|
619
|
+
|
|
620
|
+
// Legacy mode: third-party content stays in system, no wrapper added.
|
|
621
|
+
const systemJoined = parsed.system.map((b: { text: string }) => b.text).join("\n");
|
|
622
|
+
expect(systemJoined).toContain("Plugin instructions");
|
|
623
|
+
expect(parsed.messages[0].content).toBe("hi");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("does not relocate when relocateThirdPartyPrompts arg is false", () => {
|
|
627
|
+
const body = JSON.stringify({
|
|
628
|
+
model: "claude-sonnet-4-20250514",
|
|
629
|
+
messages: [{ role: "user", content: "hi" }],
|
|
630
|
+
system: [{ type: "text", text: "Plugin instructions" }],
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const result = transformRequestBody(body, mockSignature, mockRuntime, false);
|
|
634
|
+
const parsed = JSON.parse(result!);
|
|
635
|
+
|
|
636
|
+
const systemJoined = parsed.system.map((b: { text: string }) => b.text).join("\n");
|
|
637
|
+
expect(systemJoined).toContain("Plugin instructions");
|
|
638
|
+
expect(parsed.messages[0].content).toBe("hi");
|
|
639
|
+
});
|
|
632
640
|
});
|