@intx/inference-discovery-openai 0.1.2 → 0.2.2
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/LICENSE +176 -0
- package/README.md +2 -2
- package/dist/deployments/opencode-zen.d.ts +11 -0
- package/dist/deployments/opencode-zen.js +76 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/protocol/auth.d.ts +1 -0
- package/dist/protocol/auth.js +5 -0
- package/dist/protocol/body.d.ts +39 -0
- package/dist/protocol/body.js +215 -0
- package/dist/protocol/endpoint.d.ts +1 -0
- package/dist/protocol/endpoint.js +3 -0
- package/dist/protocol/iterator.d.ts +2 -0
- package/dist/protocol/iterator.js +40 -0
- package/package.json +14 -4
- package/src/deployments/opencode-zen.ts +0 -93
- package/src/index.ts +0 -6
- package/src/plugin.test.ts +0 -488
- package/src/protocol/auth.ts +0 -5
- package/src/protocol/body.ts +0 -302
- package/src/protocol/endpoint.ts +0 -3
- package/src/protocol/iterator.ts +0 -59
- package/tsconfig.json +0 -4
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { ProviderPlugin } from "@intx/inference-discovery";
|
|
2
|
-
import { buildAuthHeaders } from "../protocol/auth";
|
|
3
|
-
import { createOpenaiIterator } from "../protocol/iterator";
|
|
4
|
-
|
|
5
|
-
const PROVIDER_NAME = "opencode-zen";
|
|
6
|
-
|
|
7
|
-
const OPENCODE_ZEN_MODELS: readonly string[] = [
|
|
8
|
-
"kimi-k2.6",
|
|
9
|
-
"glm-5.1",
|
|
10
|
-
"deepseek-v4-pro",
|
|
11
|
-
"qwen3.6-plus",
|
|
12
|
-
"mimo-v2-omni",
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const REDACT_REQUEST_HEADERS: readonly string[] = ["authorization"];
|
|
16
|
-
const REDACT_RESPONSE_HEADERS: readonly string[] = [
|
|
17
|
-
"set-cookie",
|
|
18
|
-
"x-request-id",
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function lookupPath(
|
|
26
|
-
value: unknown,
|
|
27
|
-
path: readonly (string | number)[],
|
|
28
|
-
): unknown {
|
|
29
|
-
let cursor: unknown = value;
|
|
30
|
-
for (const segment of path) {
|
|
31
|
-
if (cursor === null || cursor === undefined) return undefined;
|
|
32
|
-
if (typeof segment === "number") {
|
|
33
|
-
if (!Array.isArray(cursor)) return undefined;
|
|
34
|
-
cursor = cursor[segment];
|
|
35
|
-
} else {
|
|
36
|
-
if (!isRecord(cursor)) return undefined;
|
|
37
|
-
cursor = cursor[segment];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return cursor;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isNonEmpty(value: unknown): boolean {
|
|
44
|
-
if (value === null || value === undefined) return false;
|
|
45
|
-
if (typeof value === "string") return value.length > 0;
|
|
46
|
-
if (Array.isArray(value)) return value.length > 0;
|
|
47
|
-
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface ReasoningTrace {
|
|
52
|
-
fieldPath: string;
|
|
53
|
-
sample: unknown;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// kimi-k2.6 silently routes between two upstream backends that emit
|
|
57
|
-
// reasoning under different field paths. Recording which path a given
|
|
58
|
-
// capture hit is the cheapest way to detect routing changes later.
|
|
59
|
-
const REASONING_FIELD_PATHS: readonly (readonly (string | number)[])[] = [
|
|
60
|
-
["choices", 0, "message", "reasoning_content"],
|
|
61
|
-
["choices", 0, "message", "reasoning"],
|
|
62
|
-
["choices", 0, "message", "reasoning_details"],
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
export function extractReasoningTrace(parsed: unknown): ReasoningTrace | null {
|
|
66
|
-
for (const path of REASONING_FIELD_PATHS) {
|
|
67
|
-
const value = lookupPath(parsed, path);
|
|
68
|
-
if (isNonEmpty(value)) {
|
|
69
|
-
return { fieldPath: path.join("."), sample: value };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface CreateOpencodeZenPluginOpts {
|
|
76
|
-
apiKey: string;
|
|
77
|
-
baseUrl: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function createOpencodeZenPlugin(
|
|
81
|
-
opts: CreateOpencodeZenPluginOpts,
|
|
82
|
-
): ProviderPlugin {
|
|
83
|
-
const { apiKey, baseUrl } = opts;
|
|
84
|
-
return {
|
|
85
|
-
name: PROVIDER_NAME,
|
|
86
|
-
models: OPENCODE_ZEN_MODELS,
|
|
87
|
-
redactRequestHeaders: REDACT_REQUEST_HEADERS,
|
|
88
|
-
redactResponseHeaders: REDACT_RESPONSE_HEADERS,
|
|
89
|
-
buildAuthHeaders: () => buildAuthHeaders(apiKey),
|
|
90
|
-
extractReasoningTrace,
|
|
91
|
-
iterateCaptureSteps: createOpenaiIterator(baseUrl),
|
|
92
|
-
};
|
|
93
|
-
}
|
package/src/index.ts
DELETED
package/src/plugin.test.ts
DELETED
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
INTENTS,
|
|
6
|
-
SUPPORT_MATRIX,
|
|
7
|
-
getFixtureDir,
|
|
8
|
-
type Capability,
|
|
9
|
-
type SupportEntry,
|
|
10
|
-
} from "@intx/inference-discovery/catalog";
|
|
11
|
-
import type { CaptureStep, CapturedResponse } from "@intx/inference-discovery";
|
|
12
|
-
import { createOpencodeZenPlugin } from "./index";
|
|
13
|
-
import { extractReasoningTrace } from "./deployments/opencode-zen";
|
|
14
|
-
import { createOpenaiIterator } from "./protocol/iterator";
|
|
15
|
-
import { buildRequestBody } from "./protocol/body";
|
|
16
|
-
|
|
17
|
-
const REPO_ROOT = path.resolve(import.meta.dirname, "..", "..", "..");
|
|
18
|
-
|
|
19
|
-
const TEST_API_KEY = "test-key";
|
|
20
|
-
const TEST_BASE_URL = "https://opencode.ai/zen/go/v1";
|
|
21
|
-
const TEST_CHAT_URL = `${TEST_BASE_URL}/chat/completions`;
|
|
22
|
-
|
|
23
|
-
const MULTI_TURN_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
|
|
24
|
-
"function-calling-multi-turn",
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
function makePlugin() {
|
|
28
|
-
return createOpencodeZenPlugin({
|
|
29
|
-
apiKey: TEST_API_KEY,
|
|
30
|
-
baseUrl: TEST_BASE_URL,
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function loadFixtureJSON(entry: SupportEntry, ...parts: string[]): unknown {
|
|
35
|
-
const relDir = getFixtureDir(entry);
|
|
36
|
-
if (relDir === null) {
|
|
37
|
-
throw new Error(
|
|
38
|
-
`entry has no fixture dir: ${entry.provider}/${entry.model}/${entry.capability}`,
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
const filePath = path.join(REPO_ROOT, relDir, ...parts);
|
|
42
|
-
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type Schema =
|
|
46
|
-
| { kind: "scalar" }
|
|
47
|
-
| { kind: "array"; element: Schema }
|
|
48
|
-
| { kind: "object"; fields: Record<string, Schema> }
|
|
49
|
-
| { kind: "empty-array" };
|
|
50
|
-
|
|
51
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
52
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function isScalar(value: unknown): boolean {
|
|
56
|
-
return (
|
|
57
|
-
value === null ||
|
|
58
|
-
typeof value === "boolean" ||
|
|
59
|
-
typeof value === "number" ||
|
|
60
|
-
typeof value === "string"
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function mergeSchemas(a: Schema, b: Schema): Schema {
|
|
65
|
-
if (a.kind === "empty-array") return b;
|
|
66
|
-
if (b.kind === "empty-array") return a;
|
|
67
|
-
if (a.kind !== b.kind) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
`incompatible array element schemas: ${a.kind} vs ${b.kind}`,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
if (a.kind === "object" && b.kind === "object") {
|
|
73
|
-
const merged: Record<string, Schema> = { ...a.fields };
|
|
74
|
-
for (const [key, schemaB] of Object.entries(b.fields)) {
|
|
75
|
-
const existing = merged[key];
|
|
76
|
-
merged[key] = existing ? mergeSchemas(existing, schemaB) : schemaB;
|
|
77
|
-
}
|
|
78
|
-
return { kind: "object", fields: merged };
|
|
79
|
-
}
|
|
80
|
-
if (a.kind === "array" && b.kind === "array") {
|
|
81
|
-
return { kind: "array", element: mergeSchemas(a.element, b.element) };
|
|
82
|
-
}
|
|
83
|
-
return a;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function extractSchema(value: unknown): Schema {
|
|
87
|
-
if (isScalar(value)) return { kind: "scalar" };
|
|
88
|
-
if (Array.isArray(value)) {
|
|
89
|
-
if (value.length === 0) return { kind: "empty-array" };
|
|
90
|
-
let element: Schema = extractSchema(value[0]);
|
|
91
|
-
for (let i = 1; i < value.length; i++) {
|
|
92
|
-
element = mergeSchemas(element, extractSchema(value[i]));
|
|
93
|
-
}
|
|
94
|
-
return { kind: "array", element };
|
|
95
|
-
}
|
|
96
|
-
if (isRecord(value)) {
|
|
97
|
-
const fields: Record<string, Schema> = {};
|
|
98
|
-
for (const [key, child] of Object.entries(value)) {
|
|
99
|
-
fields[key] = extractSchema(child);
|
|
100
|
-
}
|
|
101
|
-
return { kind: "object", fields };
|
|
102
|
-
}
|
|
103
|
-
throw new Error(`unsupported value type: ${typeof value}`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const EPHEMERAL_KEYS = new Set(["reasoning_content", "name", "index"]);
|
|
107
|
-
|
|
108
|
-
function pruneEphemeral(value: unknown): unknown {
|
|
109
|
-
if (Array.isArray(value)) {
|
|
110
|
-
return value.map((v) => pruneEphemeral(v));
|
|
111
|
-
}
|
|
112
|
-
if (isRecord(value)) {
|
|
113
|
-
const out: Record<string, unknown> = {};
|
|
114
|
-
for (const [key, child] of Object.entries(value)) {
|
|
115
|
-
if (EPHEMERAL_KEYS.has(key)) continue;
|
|
116
|
-
out[key] = pruneEphemeral(child);
|
|
117
|
-
}
|
|
118
|
-
return out;
|
|
119
|
-
}
|
|
120
|
-
return value;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function schemaContains(actual: Schema, expected: Schema): boolean {
|
|
124
|
-
if (expected.kind === "empty-array") {
|
|
125
|
-
return actual.kind === "array" || actual.kind === "empty-array";
|
|
126
|
-
}
|
|
127
|
-
if (actual.kind === "empty-array") {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
if (actual.kind !== expected.kind) return false;
|
|
131
|
-
if (actual.kind === "object" && expected.kind === "object") {
|
|
132
|
-
for (const [key, expectedChild] of Object.entries(expected.fields)) {
|
|
133
|
-
const actualChild = actual.fields[key];
|
|
134
|
-
if (!actualChild) return false;
|
|
135
|
-
if (!schemaContains(actualChild, expectedChild)) return false;
|
|
136
|
-
}
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
if (actual.kind === "array" && expected.kind === "array") {
|
|
140
|
-
return schemaContains(actual.element, expected.element);
|
|
141
|
-
}
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function describeSchema(schema: Schema, indent = 0): string {
|
|
146
|
-
const pad = " ".repeat(indent);
|
|
147
|
-
if (schema.kind === "object") {
|
|
148
|
-
const entries = Object.entries(schema.fields)
|
|
149
|
-
.map(([k, v]) => `${pad} ${k}: ${describeSchema(v, indent + 1)}`)
|
|
150
|
-
.join("\n");
|
|
151
|
-
return `{\n${entries}\n${pad}}`;
|
|
152
|
-
}
|
|
153
|
-
if (schema.kind === "array") {
|
|
154
|
-
return `${describeSchema(schema.element, indent)}[]`;
|
|
155
|
-
}
|
|
156
|
-
return schema.kind;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const OPENCODE_CAPTURED: SupportEntry[] = SUPPORT_MATRIX.filter(
|
|
160
|
-
(e) => e.provider === "opencode-zen" && e.outcome === "captured",
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
function collectSteps(opts: {
|
|
164
|
-
model: string;
|
|
165
|
-
capability: Capability;
|
|
166
|
-
responses: readonly CapturedResponse[];
|
|
167
|
-
}): CaptureStep[] {
|
|
168
|
-
const intent = INTENTS[opts.capability];
|
|
169
|
-
const iter = createOpenaiIterator(TEST_BASE_URL)({
|
|
170
|
-
model: opts.model,
|
|
171
|
-
capability: opts.capability,
|
|
172
|
-
intent,
|
|
173
|
-
});
|
|
174
|
-
const steps: CaptureStep[] = [];
|
|
175
|
-
let i = 0;
|
|
176
|
-
let next = iter.next();
|
|
177
|
-
while (!next.done) {
|
|
178
|
-
steps.push(next.value);
|
|
179
|
-
const response = opts.responses[i];
|
|
180
|
-
i += 1;
|
|
181
|
-
if (response === undefined) break;
|
|
182
|
-
next = iter.next(response);
|
|
183
|
-
}
|
|
184
|
-
return steps;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
describe("createOpencodeZenPlugin", () => {
|
|
188
|
-
test("exposes the five OpenCode-Zen models", () => {
|
|
189
|
-
const plugin = makePlugin();
|
|
190
|
-
expect(plugin.name).toBe("opencode-zen");
|
|
191
|
-
expect([...plugin.models].sort()).toEqual(
|
|
192
|
-
[
|
|
193
|
-
"deepseek-v4-pro",
|
|
194
|
-
"glm-5.1",
|
|
195
|
-
"kimi-k2.6",
|
|
196
|
-
"mimo-v2-omni",
|
|
197
|
-
"qwen3.6-plus",
|
|
198
|
-
].sort(),
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("redacts the Authorization header", () => {
|
|
203
|
-
const plugin = makePlugin();
|
|
204
|
-
expect(plugin.redactRequestHeaders).toContain("authorization");
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("buildAuthHeaders attaches Bearer token", () => {
|
|
208
|
-
const plugin = makePlugin();
|
|
209
|
-
const headers = plugin.buildAuthHeaders();
|
|
210
|
-
expect(headers.Authorization).toBe("Bearer test-key");
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
describe("buildRequestBody capability dispatch", () => {
|
|
215
|
-
test("plain-text returns user message with no stream flag", () => {
|
|
216
|
-
const body = buildRequestBody({
|
|
217
|
-
model: "kimi-k2.6",
|
|
218
|
-
capability: "plain-text",
|
|
219
|
-
intent: INTENTS["plain-text"],
|
|
220
|
-
});
|
|
221
|
-
if (!isRecord(body)) throw new Error("expected record body");
|
|
222
|
-
expect(body.model).toBe("kimi-k2.6");
|
|
223
|
-
expect(body.stream).toBeUndefined();
|
|
224
|
-
const messages = body.messages;
|
|
225
|
-
if (!Array.isArray(messages)) throw new Error("expected messages array");
|
|
226
|
-
expect(messages.length).toBe(1);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test("plain-text-streaming sets stream: true", () => {
|
|
230
|
-
const body = buildRequestBody({
|
|
231
|
-
model: "kimi-k2.6",
|
|
232
|
-
capability: "plain-text-streaming",
|
|
233
|
-
intent: INTENTS["plain-text-streaming"],
|
|
234
|
-
});
|
|
235
|
-
if (!isRecord(body)) throw new Error("expected record body");
|
|
236
|
-
expect(body.stream).toBe(true);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
test("function-calling produces messages + tools", () => {
|
|
240
|
-
const body = buildRequestBody({
|
|
241
|
-
model: "kimi-k2.6",
|
|
242
|
-
capability: "function-calling",
|
|
243
|
-
intent: INTENTS["function-calling"],
|
|
244
|
-
});
|
|
245
|
-
if (!isRecord(body)) throw new Error("expected record body");
|
|
246
|
-
expect(Array.isArray(body.tools)).toBe(true);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("function-calling-multi-turn throws (multi-step capability)", () => {
|
|
250
|
-
expect(() =>
|
|
251
|
-
buildRequestBody({
|
|
252
|
-
model: "kimi-k2.6",
|
|
253
|
-
capability: "function-calling-multi-turn",
|
|
254
|
-
intent: INTENTS["function-calling-multi-turn"],
|
|
255
|
-
}),
|
|
256
|
-
).toThrow(/multi-step capability/);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
test("vision-input embeds image as data URI", () => {
|
|
260
|
-
const body = buildRequestBody({
|
|
261
|
-
model: "kimi-k2.6",
|
|
262
|
-
capability: "vision-input",
|
|
263
|
-
intent: INTENTS["vision-input"],
|
|
264
|
-
});
|
|
265
|
-
if (!isRecord(body)) throw new Error("expected record body");
|
|
266
|
-
const messages = body.messages;
|
|
267
|
-
if (!Array.isArray(messages) || !isRecord(messages[0])) {
|
|
268
|
-
throw new Error("expected messages[0] to be a record");
|
|
269
|
-
}
|
|
270
|
-
const content = messages[0].content;
|
|
271
|
-
if (!Array.isArray(content)) throw new Error("expected content array");
|
|
272
|
-
const imagePart = content.find(
|
|
273
|
-
(p) => isRecord(p) && p.type === "image_url",
|
|
274
|
-
);
|
|
275
|
-
if (!isRecord(imagePart)) throw new Error("expected image_url part");
|
|
276
|
-
const imageUrl = imagePart.image_url;
|
|
277
|
-
if (!isRecord(imageUrl)) throw new Error("expected image_url record");
|
|
278
|
-
expect(typeof imageUrl.url).toBe("string");
|
|
279
|
-
expect(String(imageUrl.url).startsWith("data:image/jpeg;base64,")).toBe(
|
|
280
|
-
true,
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("reasoning-content uses user message only", () => {
|
|
285
|
-
const body = buildRequestBody({
|
|
286
|
-
model: "kimi-k2.6",
|
|
287
|
-
capability: "reasoning-content",
|
|
288
|
-
intent: INTENTS["reasoning-content"],
|
|
289
|
-
});
|
|
290
|
-
if (!isRecord(body)) throw new Error("expected record body");
|
|
291
|
-
expect(body.stream).toBeUndefined();
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("reasoning-content-streaming sets stream: true", () => {
|
|
295
|
-
const body = buildRequestBody({
|
|
296
|
-
model: "kimi-k2.6",
|
|
297
|
-
capability: "reasoning-content-streaming",
|
|
298
|
-
intent: INTENTS["reasoning-content-streaming"],
|
|
299
|
-
});
|
|
300
|
-
if (!isRecord(body)) throw new Error("expected record body");
|
|
301
|
-
expect(body.stream).toBe(true);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test("throws on capability not in opencode-zen support set", () => {
|
|
305
|
-
expect(() =>
|
|
306
|
-
buildRequestBody({
|
|
307
|
-
model: "kimi-k2.6",
|
|
308
|
-
capability: "audio-input",
|
|
309
|
-
intent: INTENTS["audio-input"],
|
|
310
|
-
}),
|
|
311
|
-
).toThrow();
|
|
312
|
-
});
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
describe("fixture oracle: every captured (model, capability) matches structure", () => {
|
|
316
|
-
test("there is at least one captured opencode-zen entry", () => {
|
|
317
|
-
expect(OPENCODE_CAPTURED.length).toBeGreaterThan(0);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
for (const entry of OPENCODE_CAPTURED) {
|
|
321
|
-
test(`structural match: ${entry.model} / ${entry.capability}`, () => {
|
|
322
|
-
if (MULTI_TURN_CAPABILITIES.has(entry.capability)) {
|
|
323
|
-
const turn1Response: CapturedResponse = {
|
|
324
|
-
status: 200,
|
|
325
|
-
headers: {},
|
|
326
|
-
parsed: loadFixtureJSON(entry, "turn-1", "response.json"),
|
|
327
|
-
bytes: null,
|
|
328
|
-
};
|
|
329
|
-
const steps = collectSteps({
|
|
330
|
-
model: entry.model,
|
|
331
|
-
capability: entry.capability,
|
|
332
|
-
responses: [turn1Response],
|
|
333
|
-
});
|
|
334
|
-
expect(steps.length).toBe(2);
|
|
335
|
-
const [step1, step2] = steps;
|
|
336
|
-
if (step1 === undefined || step2 === undefined) {
|
|
337
|
-
throw new Error("expected two steps for multi-turn");
|
|
338
|
-
}
|
|
339
|
-
expect(step1.subdir).toBe("turn-1");
|
|
340
|
-
expect(step2.subdir).toBe("turn-2");
|
|
341
|
-
expect(step1.url).toBe(TEST_CHAT_URL);
|
|
342
|
-
expect(step2.url).toBe(TEST_CHAT_URL);
|
|
343
|
-
|
|
344
|
-
const captured1 = loadFixtureJSON(entry, "turn-1", "request.json");
|
|
345
|
-
const captured2 = loadFixtureJSON(entry, "turn-2", "request.json");
|
|
346
|
-
|
|
347
|
-
const cap1Schema = extractSchema(pruneEphemeral(captured1));
|
|
348
|
-
const built1Schema = extractSchema(pruneEphemeral(step1.body));
|
|
349
|
-
if (!schemaContains(built1Schema, cap1Schema)) {
|
|
350
|
-
throw new Error(
|
|
351
|
-
[
|
|
352
|
-
"turn-1 schema mismatch",
|
|
353
|
-
`captured: ${describeSchema(cap1Schema)}`,
|
|
354
|
-
`built: ${describeSchema(built1Schema)}`,
|
|
355
|
-
].join("\n"),
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const cap2Schema = extractSchema(pruneEphemeral(captured2));
|
|
360
|
-
const built2Schema = extractSchema(pruneEphemeral(step2.body));
|
|
361
|
-
if (!schemaContains(built2Schema, cap2Schema)) {
|
|
362
|
-
throw new Error(
|
|
363
|
-
[
|
|
364
|
-
"turn-2 schema mismatch",
|
|
365
|
-
`captured: ${describeSchema(cap2Schema)}`,
|
|
366
|
-
`built: ${describeSchema(built2Schema)}`,
|
|
367
|
-
].join("\n"),
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const captured = loadFixtureJSON(entry, "request.json");
|
|
374
|
-
const steps = collectSteps({
|
|
375
|
-
model: entry.model,
|
|
376
|
-
capability: entry.capability,
|
|
377
|
-
responses: [],
|
|
378
|
-
});
|
|
379
|
-
expect(steps.length).toBe(1);
|
|
380
|
-
const [only] = steps;
|
|
381
|
-
if (only === undefined) throw new Error("expected one step");
|
|
382
|
-
expect(only.subdir).toBeNull();
|
|
383
|
-
expect(only.url).toBe(TEST_CHAT_URL);
|
|
384
|
-
|
|
385
|
-
const capturedSchema = extractSchema(pruneEphemeral(captured));
|
|
386
|
-
const builtSchema = extractSchema(pruneEphemeral(only.body));
|
|
387
|
-
|
|
388
|
-
const ok = schemaContains(builtSchema, capturedSchema);
|
|
389
|
-
if (!ok) {
|
|
390
|
-
const msg = [
|
|
391
|
-
"schema mismatch",
|
|
392
|
-
`captured: ${describeSchema(capturedSchema)}`,
|
|
393
|
-
`built: ${describeSchema(builtSchema)}`,
|
|
394
|
-
].join("\n");
|
|
395
|
-
throw new Error(msg);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (!isRecord(only.body)) {
|
|
399
|
-
throw new Error("expected built body to be record");
|
|
400
|
-
}
|
|
401
|
-
expect(only.body.model).toBe(entry.model);
|
|
402
|
-
|
|
403
|
-
const wantsStream = entry.capability.endsWith("-streaming");
|
|
404
|
-
if (wantsStream) {
|
|
405
|
-
expect(only.body.stream).toBe(true);
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
describe("extractReasoningTrace", () => {
|
|
412
|
-
test("returns trace for Moonshot-style reasoning_details", () => {
|
|
413
|
-
const parsed = {
|
|
414
|
-
choices: [
|
|
415
|
-
{
|
|
416
|
-
message: {
|
|
417
|
-
reasoning_details: [{ text: "step one" }],
|
|
418
|
-
},
|
|
419
|
-
},
|
|
420
|
-
],
|
|
421
|
-
};
|
|
422
|
-
const trace = extractReasoningTrace(parsed);
|
|
423
|
-
expect(trace).not.toBeNull();
|
|
424
|
-
if (trace === null) throw new Error("expected trace");
|
|
425
|
-
expect(trace.fieldPath).toBe("choices.0.message.reasoning_details");
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
test("returns trace for Moonshot-style reasoning field", () => {
|
|
429
|
-
const parsed = {
|
|
430
|
-
choices: [
|
|
431
|
-
{
|
|
432
|
-
message: {
|
|
433
|
-
reasoning: "thought process",
|
|
434
|
-
},
|
|
435
|
-
},
|
|
436
|
-
],
|
|
437
|
-
};
|
|
438
|
-
const trace = extractReasoningTrace(parsed);
|
|
439
|
-
expect(trace).not.toBeNull();
|
|
440
|
-
if (trace === null) throw new Error("expected trace");
|
|
441
|
-
expect(trace.fieldPath).toBe("choices.0.message.reasoning");
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("returns trace for Fireworks-style reasoning_content", () => {
|
|
445
|
-
const parsed = {
|
|
446
|
-
choices: [
|
|
447
|
-
{
|
|
448
|
-
message: {
|
|
449
|
-
reasoning_content: "chain of thought",
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
};
|
|
454
|
-
const trace = extractReasoningTrace(parsed);
|
|
455
|
-
expect(trace).not.toBeNull();
|
|
456
|
-
if (trace === null) throw new Error("expected trace");
|
|
457
|
-
expect(trace.fieldPath).toBe("choices.0.message.reasoning_content");
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
test("returns null when no reasoning fields present", () => {
|
|
461
|
-
const parsed = {
|
|
462
|
-
choices: [{ message: { content: "regular text" } }],
|
|
463
|
-
};
|
|
464
|
-
expect(extractReasoningTrace(parsed)).toBeNull();
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
test("returns null for empty reasoning fields", () => {
|
|
468
|
-
const parsed = {
|
|
469
|
-
choices: [{ message: { reasoning: "" } }],
|
|
470
|
-
};
|
|
471
|
-
expect(extractReasoningTrace(parsed)).toBeNull();
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
test("returns null for malformed input", () => {
|
|
475
|
-
expect(extractReasoningTrace(null)).toBeNull();
|
|
476
|
-
expect(extractReasoningTrace("string")).toBeNull();
|
|
477
|
-
expect(extractReasoningTrace({})).toBeNull();
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
describe("plugin via stub fetch", () => {
|
|
482
|
-
test("plug-in returned by factory is callable and well-formed", () => {
|
|
483
|
-
const plugin = makePlugin();
|
|
484
|
-
expect(typeof plugin.buildAuthHeaders).toBe("function");
|
|
485
|
-
expect(typeof plugin.iterateCaptureSteps).toBe("function");
|
|
486
|
-
expect(typeof plugin.extractReasoningTrace).toBe("function");
|
|
487
|
-
});
|
|
488
|
-
});
|