pi-provider-kimi-code 0.3.0 → 0.3.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/README.md +99 -51
- package/index.ts +42 -883
- package/package.json +10 -6
- package/src/constants.ts +48 -0
- package/src/device.ts +164 -0
- package/src/models.ts +129 -0
- package/src/oauth.ts +397 -0
- package/src/payload.ts +446 -0
- package/src/stream.ts +299 -0
package/src/payload.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// Payload pipeline: shared types, pure utilities, file-upload IO edge,
|
|
2
|
+
// per-protocol message transforms, OpenAI tool-call / tool-schema normalizers,
|
|
3
|
+
// and the top-level applyKimiPayloadMutations that orchestrates them.
|
|
4
|
+
|
|
5
|
+
import type { CacheRetention, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
6
|
+
|
|
7
|
+
import { getBaseUrl } from "./constants.ts";
|
|
8
|
+
import { getCommonHeaders } from "./device.ts";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Shared types + small utilities
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
const DEFAULT_KIMI_INLINE_UPLOAD_THRESHOLD_BYTES = 1 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export type JsonRecord = Record<string, unknown>;
|
|
17
|
+
export type Uploader = (mimeType: string, data: string) => Promise<string | null>;
|
|
18
|
+
|
|
19
|
+
export interface KimiEnvOverrides {
|
|
20
|
+
temperature?: number;
|
|
21
|
+
topP?: number;
|
|
22
|
+
maxTokens?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveCacheRetention(value?: CacheRetention): CacheRetention {
|
|
26
|
+
if (value === "none" || value === "short" || value === "long") return value;
|
|
27
|
+
const envRetention = process.env.PI_CACHE_RETENTION;
|
|
28
|
+
if (envRetention === "none" || envRetention === "short" || envRetention === "long") {
|
|
29
|
+
return envRetention;
|
|
30
|
+
}
|
|
31
|
+
return "short";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface KimiPayloadContext {
|
|
35
|
+
api: "anthropic-messages" | "openai-completions";
|
|
36
|
+
upload?: Uploader;
|
|
37
|
+
cacheKey?: string;
|
|
38
|
+
cacheRetention: CacheRetention;
|
|
39
|
+
reasoning?: ThinkingLevel;
|
|
40
|
+
thinkingKeep?: string;
|
|
41
|
+
envOverrides: KimiEnvOverrides;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isRecord(value: unknown): value is JsonRecord {
|
|
45
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mapThinkingLevel(level?: string): { effort: string | null; enabled: boolean } | undefined {
|
|
49
|
+
if (!level) return undefined;
|
|
50
|
+
if (level === "none" || level === "off") return { effort: null, enabled: false };
|
|
51
|
+
if (level === "minimal" || level === "low") return { effort: "low", enabled: true };
|
|
52
|
+
if (level === "medium") return { effort: "medium", enabled: true };
|
|
53
|
+
if (level === "high" || level === "xhigh") return { effort: "high", enabled: true };
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseInlineUploadThreshold(raw: string | undefined): number {
|
|
58
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
59
|
+
return Number.isFinite(parsed) && parsed >= 0
|
|
60
|
+
? parsed
|
|
61
|
+
: DEFAULT_KIMI_INLINE_UPLOAD_THRESHOLD_BYTES;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function deriveFilesBaseUrl(baseUrl: string): string {
|
|
65
|
+
const trimmed = baseUrl.replace(/\/$/, "");
|
|
66
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseDataUrl(url: string): { mimeType: string; data: string } | null {
|
|
70
|
+
const match = url.match(/^data:([^;,]+);base64,([A-Za-z0-9+/=]+)$/);
|
|
71
|
+
return match ? { mimeType: match[1], data: match[2] } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getUploadFilename(mimeType: string): string {
|
|
75
|
+
const map: Record<string, string> = {
|
|
76
|
+
"image/jpeg": "upload.jpg",
|
|
77
|
+
"image/png": "upload.png",
|
|
78
|
+
"image/gif": "upload.gif",
|
|
79
|
+
"image/webp": "upload.webp",
|
|
80
|
+
};
|
|
81
|
+
return map[mimeType] ?? "upload.bin";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readEnvOverrides(): KimiEnvOverrides {
|
|
85
|
+
const out: KimiEnvOverrides = {};
|
|
86
|
+
const temp = process.env.KIMI_MODEL_TEMPERATURE;
|
|
87
|
+
if (temp) out.temperature = parseFloat(temp);
|
|
88
|
+
const topP = process.env.KIMI_MODEL_TOP_P;
|
|
89
|
+
if (topP) out.topP = parseFloat(topP);
|
|
90
|
+
const maxTokens = process.env.KIMI_MODEL_MAX_TOKENS;
|
|
91
|
+
if (maxTokens) out.maxTokens = parseInt(maxTokens, 10);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// =============================================================================
|
|
96
|
+
// File upload (I/O edge)
|
|
97
|
+
// =============================================================================
|
|
98
|
+
|
|
99
|
+
export async function uploadKimiFile(
|
|
100
|
+
apiKey: string,
|
|
101
|
+
mimeType: string,
|
|
102
|
+
data: string,
|
|
103
|
+
): Promise<string | null> {
|
|
104
|
+
const buffer = Buffer.from(data, "base64");
|
|
105
|
+
if (!mimeType.startsWith("image/")) return null;
|
|
106
|
+
const threshold = parseInlineUploadThreshold(process.env.KIMI_CODE_UPLOAD_THRESHOLD_BYTES);
|
|
107
|
+
if (buffer.length <= threshold) return null;
|
|
108
|
+
|
|
109
|
+
const filename = getUploadFilename(mimeType);
|
|
110
|
+
const formData = new FormData();
|
|
111
|
+
formData.append("file", new Blob([buffer], { type: mimeType }), filename);
|
|
112
|
+
formData.append("purpose", "image");
|
|
113
|
+
|
|
114
|
+
const uploadUrl = `${deriveFilesBaseUrl(getBaseUrl())}/files`;
|
|
115
|
+
const debug = process.env.KIMI_CODE_DEBUG === "1";
|
|
116
|
+
if (debug) {
|
|
117
|
+
console.log(
|
|
118
|
+
`\n[kimi-coding] Uploading ${filename} to ${uploadUrl} (${(buffer.length / 1024 / 1024).toFixed(2)} MB)`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetch(uploadUrl, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { Authorization: `Bearer ${apiKey}`, ...getCommonHeaders() },
|
|
126
|
+
body: formData,
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
|
|
129
|
+
const fileObj = (await response.json()) as { id?: string };
|
|
130
|
+
if (!fileObj.id) throw new Error("missing file id");
|
|
131
|
+
const fileUrl = `ms://${fileObj.id}`;
|
|
132
|
+
if (debug) console.log(`[kimi-coding] Upload success: ${fileUrl}`);
|
|
133
|
+
return fileUrl;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error("[kimi-coding] Upload failed:", err);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// Payload file transformers (pure given an Uploader)
|
|
142
|
+
//
|
|
143
|
+
// These walk the provider-specific payload shape and replace inline base64
|
|
144
|
+
// image blocks with ms:// references returned by the injected uploader. They
|
|
145
|
+
// take an Uploader rather than an apiKey so they can be unit-tested with a
|
|
146
|
+
// fake uploader; all network I/O stays behind that boundary.
|
|
147
|
+
// =============================================================================
|
|
148
|
+
|
|
149
|
+
async function transformOpenAIPayloadFiles(payload: JsonRecord, upload: Uploader): Promise<void> {
|
|
150
|
+
if (!Array.isArray(payload.messages)) return;
|
|
151
|
+
const cache = new Map<string, string>();
|
|
152
|
+
|
|
153
|
+
for (const message of payload.messages) {
|
|
154
|
+
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
155
|
+
|
|
156
|
+
for (const block of message.content) {
|
|
157
|
+
if (!isRecord(block)) continue;
|
|
158
|
+
const key = block.type === "image_url" ? "image_url" : null;
|
|
159
|
+
if (!key) continue;
|
|
160
|
+
|
|
161
|
+
const field = block[key];
|
|
162
|
+
const urlValue =
|
|
163
|
+
typeof field === "string"
|
|
164
|
+
? field
|
|
165
|
+
: isRecord(field) && typeof field.url === "string"
|
|
166
|
+
? field.url
|
|
167
|
+
: null;
|
|
168
|
+
if (!urlValue || urlValue.startsWith("ms://")) continue;
|
|
169
|
+
|
|
170
|
+
const parsed = parseDataUrl(urlValue);
|
|
171
|
+
if (!parsed) continue;
|
|
172
|
+
|
|
173
|
+
const uploaded = cache.get(urlValue) ?? (await upload(parsed.mimeType, parsed.data));
|
|
174
|
+
if (!uploaded) continue;
|
|
175
|
+
cache.set(urlValue, uploaded);
|
|
176
|
+
|
|
177
|
+
block[key] =
|
|
178
|
+
typeof field === "string" ? uploaded : { ...(field as JsonRecord), url: uploaded };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isEffectivelyEmptyOpenAIContent(content: unknown): boolean {
|
|
184
|
+
if (typeof content === "string") return content.trim() === "";
|
|
185
|
+
if (!Array.isArray(content)) return false;
|
|
186
|
+
for (const part of content) {
|
|
187
|
+
if (!isRecord(part) || part.type !== "text") return false;
|
|
188
|
+
if (typeof part.text === "string" && part.text.trim()) return false;
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeOpenAIAssistantToolCalls(payload: JsonRecord): void {
|
|
194
|
+
if (!Array.isArray(payload.messages)) return;
|
|
195
|
+
for (const message of payload.messages) {
|
|
196
|
+
if (!isRecord(message) || message.role !== "assistant") continue;
|
|
197
|
+
if (!Array.isArray(message.tool_calls) || message.tool_calls.length === 0) continue;
|
|
198
|
+
if (isEffectivelyEmptyOpenAIContent(message.content)) {
|
|
199
|
+
delete message.content;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// -----------------------------------------------------------------------------
|
|
205
|
+
// JSON Schema property-type normalizer (mirrors kosong's ensure_property_types).
|
|
206
|
+
// Moonshot's tool schema validator rejects property schemas that omit `type`;
|
|
207
|
+
// this walks the schema and back-fills a type from `enum` / `const` / nested
|
|
208
|
+
// structure hints, defaulting to "string" when nothing else applies.
|
|
209
|
+
// -----------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
const JSON_SCHEMA_COMBINATOR_KEYS = new Set([
|
|
212
|
+
"anyOf",
|
|
213
|
+
"oneOf",
|
|
214
|
+
"allOf",
|
|
215
|
+
"not",
|
|
216
|
+
"if",
|
|
217
|
+
"then",
|
|
218
|
+
"else",
|
|
219
|
+
"$ref",
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const JSON_SCHEMA_OBJECT_KEYS = new Set([
|
|
223
|
+
"properties",
|
|
224
|
+
"additionalProperties",
|
|
225
|
+
"patternProperties",
|
|
226
|
+
"propertyNames",
|
|
227
|
+
"required",
|
|
228
|
+
"minProperties",
|
|
229
|
+
"maxProperties",
|
|
230
|
+
]);
|
|
231
|
+
const JSON_SCHEMA_ARRAY_KEYS = new Set([
|
|
232
|
+
"items",
|
|
233
|
+
"prefixItems",
|
|
234
|
+
"minItems",
|
|
235
|
+
"maxItems",
|
|
236
|
+
"uniqueItems",
|
|
237
|
+
"contains",
|
|
238
|
+
]);
|
|
239
|
+
const JSON_SCHEMA_STRING_KEYS = new Set(["minLength", "maxLength", "pattern", "format"]);
|
|
240
|
+
const JSON_SCHEMA_NUMERIC_KEYS = new Set([
|
|
241
|
+
"minimum",
|
|
242
|
+
"maximum",
|
|
243
|
+
"multipleOf",
|
|
244
|
+
"exclusiveMinimum",
|
|
245
|
+
"exclusiveMaximum",
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
function hasAnyKey(record: JsonRecord, keys: Set<string>): boolean {
|
|
249
|
+
return Object.keys(record).some((key) => keys.has(key));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function inferJsonSchemaTypeFromValues(values: unknown[]): string {
|
|
253
|
+
const inferred = new Set<string>();
|
|
254
|
+
for (const value of values) {
|
|
255
|
+
if (typeof value === "boolean") inferred.add("boolean");
|
|
256
|
+
else if (typeof value === "number")
|
|
257
|
+
inferred.add(Number.isInteger(value) ? "integer" : "number");
|
|
258
|
+
else if (typeof value === "string") inferred.add("string");
|
|
259
|
+
else if (value === null) inferred.add("null");
|
|
260
|
+
else if (Array.isArray(value)) inferred.add("array");
|
|
261
|
+
else if (isRecord(value)) inferred.add("object");
|
|
262
|
+
else return "string";
|
|
263
|
+
}
|
|
264
|
+
if (inferred.size === 1) return [...inferred][0] ?? "string";
|
|
265
|
+
if (inferred.size === 2 && inferred.has("integer") && inferred.has("number")) return "number";
|
|
266
|
+
return "string";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function inferJsonSchemaTypeFromStructure(node: JsonRecord): string {
|
|
270
|
+
if (hasAnyKey(node, JSON_SCHEMA_OBJECT_KEYS)) return "object";
|
|
271
|
+
if (hasAnyKey(node, JSON_SCHEMA_ARRAY_KEYS)) return "array";
|
|
272
|
+
if (hasAnyKey(node, JSON_SCHEMA_STRING_KEYS)) return "string";
|
|
273
|
+
if (hasAnyKey(node, JSON_SCHEMA_NUMERIC_KEYS)) return "number";
|
|
274
|
+
return "string";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeJsonSchemaPropertyTypes(node: unknown): void {
|
|
278
|
+
if (!isRecord(node)) return;
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
node.type === undefined &&
|
|
282
|
+
!Object.keys(node).some((key) => JSON_SCHEMA_COMBINATOR_KEYS.has(key))
|
|
283
|
+
) {
|
|
284
|
+
if (Array.isArray(node.enum) && node.enum.length > 0) {
|
|
285
|
+
node.type = inferJsonSchemaTypeFromValues(node.enum);
|
|
286
|
+
} else if ("const" in node) {
|
|
287
|
+
node.type = inferJsonSchemaTypeFromValues([node.const]);
|
|
288
|
+
} else {
|
|
289
|
+
node.type = inferJsonSchemaTypeFromStructure(node);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
recurseJsonSchemaPropertyTypes(node);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function recurseJsonSchemaPropertyTypes(node: unknown): void {
|
|
297
|
+
if (!isRecord(node)) return;
|
|
298
|
+
|
|
299
|
+
if (isRecord(node.properties)) {
|
|
300
|
+
for (const value of Object.values(node.properties)) {
|
|
301
|
+
normalizeJsonSchemaPropertyTypes(value);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (isRecord(node.items)) {
|
|
306
|
+
normalizeJsonSchemaPropertyTypes(node.items);
|
|
307
|
+
} else if (Array.isArray(node.items)) {
|
|
308
|
+
for (const value of node.items) {
|
|
309
|
+
normalizeJsonSchemaPropertyTypes(value);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (isRecord(node.additionalProperties)) {
|
|
314
|
+
normalizeJsonSchemaPropertyTypes(node.additionalProperties);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
318
|
+
const branches = node[key];
|
|
319
|
+
if (!Array.isArray(branches)) continue;
|
|
320
|
+
for (const value of branches) {
|
|
321
|
+
normalizeJsonSchemaPropertyTypes(value);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function normalizeOpenAIToolSchemas(payload: JsonRecord): void {
|
|
327
|
+
if (!Array.isArray(payload.tools)) return;
|
|
328
|
+
for (const tool of payload.tools) {
|
|
329
|
+
if (!isRecord(tool) || !isRecord(tool.function)) continue;
|
|
330
|
+
const parameters = tool.function.parameters;
|
|
331
|
+
if (!isRecord(parameters)) continue;
|
|
332
|
+
recurseJsonSchemaPropertyTypes(parameters);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function transformAnthropicPayloadFiles(
|
|
337
|
+
payload: JsonRecord,
|
|
338
|
+
upload: Uploader,
|
|
339
|
+
): Promise<void> {
|
|
340
|
+
if (!Array.isArray(payload.messages)) return;
|
|
341
|
+
const cache = new Map<string, string>();
|
|
342
|
+
|
|
343
|
+
const transformImageBlock = async (block: unknown): Promise<unknown> => {
|
|
344
|
+
if (!isRecord(block) || block.type !== "image") return block;
|
|
345
|
+
const source = block.source;
|
|
346
|
+
if (!isRecord(source) || source.type !== "base64") return block;
|
|
347
|
+
const mediaType = source.media_type;
|
|
348
|
+
const data = source.data;
|
|
349
|
+
if (typeof mediaType !== "string" || typeof data !== "string") return block;
|
|
350
|
+
|
|
351
|
+
const cacheKey = `${mediaType}:${data}`;
|
|
352
|
+
const uploaded = cache.get(cacheKey) ?? (await upload(mediaType, data));
|
|
353
|
+
if (!uploaded) return block;
|
|
354
|
+
cache.set(cacheKey, uploaded);
|
|
355
|
+
|
|
356
|
+
const next: JsonRecord = { type: "image", source: { type: "url", url: uploaded } };
|
|
357
|
+
if (block.cache_control !== undefined) next.cache_control = block.cache_control;
|
|
358
|
+
return next;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
for (const message of payload.messages) {
|
|
362
|
+
if (!isRecord(message) || !Array.isArray(message.content)) continue;
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
365
|
+
const block = message.content[i];
|
|
366
|
+
if (isRecord(block) && block.type === "tool_result" && Array.isArray(block.content)) {
|
|
367
|
+
for (let j = 0; j < block.content.length; j++) {
|
|
368
|
+
block.content[j] = await transformImageBlock(block.content[j]);
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
message.content[i] = await transformImageBlock(block);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// =============================================================================
|
|
378
|
+
// Payload mutation pipeline
|
|
379
|
+
//
|
|
380
|
+
// Applies all Kimi-specific mutations to a provider payload in place. Pure
|
|
381
|
+
// given its context: no process.env / fs / network access of its own — every
|
|
382
|
+
// side effect enters via ctx.upload or pre-read values in ctx. This makes the
|
|
383
|
+
// steps below testable with fixture payloads.
|
|
384
|
+
// =============================================================================
|
|
385
|
+
|
|
386
|
+
export async function applyKimiPayloadMutations(
|
|
387
|
+
payload: JsonRecord,
|
|
388
|
+
ctx: KimiPayloadContext,
|
|
389
|
+
): Promise<void> {
|
|
390
|
+
// 1. Map unsupported roles: Kimi does not recognize "developer" (OpenAI-specific).
|
|
391
|
+
if (Array.isArray(payload.messages)) {
|
|
392
|
+
payload.messages = payload.messages.map((msg) =>
|
|
393
|
+
isRecord(msg) && msg.role === "developer" ? { ...msg, role: "system" } : msg,
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 2. File upload dispatch (protocol-specific).
|
|
398
|
+
if (ctx.upload) {
|
|
399
|
+
if (ctx.api === "openai-completions") {
|
|
400
|
+
await transformOpenAIPayloadFiles(payload, ctx.upload);
|
|
401
|
+
} else if (ctx.api === "anthropic-messages") {
|
|
402
|
+
await transformAnthropicPayloadFiles(payload, ctx.upload);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (ctx.api === "openai-completions") {
|
|
406
|
+
normalizeOpenAIAssistantToolCalls(payload);
|
|
407
|
+
normalizeOpenAIToolSchemas(payload);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 3. prompt_cache_key injection. Respect any key already on the payload,
|
|
411
|
+
// otherwise fall back to the caller-provided cacheKey (sessionId or
|
|
412
|
+
// explicit options.prompt_cache_key override). Skipped entirely when
|
|
413
|
+
// cacheRetention is "none" (via options.cacheRetention or
|
|
414
|
+
// PI_CACHE_RETENTION) so callers can truly disable caching — otherwise
|
|
415
|
+
// Kimi's native session cache would still fire even if pi-ai's
|
|
416
|
+
// Anthropic-style cache_control markers are omitted.
|
|
417
|
+
if (ctx.cacheRetention !== "none") {
|
|
418
|
+
const existing = payload.prompt_cache_key;
|
|
419
|
+
const resolved = (typeof existing === "string" && existing) || ctx.cacheKey;
|
|
420
|
+
if (resolved) payload.prompt_cache_key = resolved;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// 4. Env-level hyperparameter overrides (pre-parsed into numbers by caller).
|
|
424
|
+
const { temperature, topP, maxTokens } = ctx.envOverrides;
|
|
425
|
+
if (temperature !== undefined) payload.temperature = temperature;
|
|
426
|
+
if (topP !== undefined) payload.top_p = topP;
|
|
427
|
+
if (maxTokens !== undefined) payload.max_tokens = maxTokens;
|
|
428
|
+
|
|
429
|
+
// 5. Reasoning effort mapping.
|
|
430
|
+
if (ctx.reasoning) {
|
|
431
|
+
const mapped = mapThinkingLevel(ctx.reasoning);
|
|
432
|
+
if (mapped) {
|
|
433
|
+
payload.reasoning_effort = mapped.effort;
|
|
434
|
+
const extraBody = isRecord(payload.extra_body) ? payload.extra_body : {};
|
|
435
|
+
const oldThinking = isRecord(extraBody.thinking) ? extraBody.thinking : {};
|
|
436
|
+
extraBody.thinking = {
|
|
437
|
+
...oldThinking,
|
|
438
|
+
type: mapped.enabled ? "enabled" : "disabled",
|
|
439
|
+
};
|
|
440
|
+
if (mapped.enabled && ctx.thinkingKeep) {
|
|
441
|
+
(extraBody.thinking as JsonRecord).keep = ctx.thinkingKeep;
|
|
442
|
+
}
|
|
443
|
+
payload.extra_body = extraBody;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|