opencode-cmd-provider 1.2.2 → 1.4.0
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/CHANGELOG.md +77 -0
- package/README.md +2 -0
- package/dist/src/deals/catalog.js +2 -0
- package/dist/src/deals/plan-summary.d.ts +33 -1
- package/dist/src/deals/plan-summary.js +35 -19
- package/dist/src/env.d.ts +9 -0
- package/dist/src/env.js +11 -0
- package/dist/src/plugin/auth-mirror.d.ts +11 -0
- package/dist/src/plugin/auth-mirror.js +74 -0
- package/dist/src/plugin/auth.d.ts +3 -0
- package/dist/src/plugin/auth.js +12 -0
- package/dist/src/provider/command-code-model.d.ts +49 -0
- package/dist/src/provider/command-code-model.js +419 -171
- package/dist/src/provider/converters.d.ts +18 -0
- package/dist/src/provider/converters.js +464 -3
- package/dist/src/provider/redact.d.ts +11 -0
- package/dist/src/provider/redact.js +38 -0
- package/dist/src/provider/stream.d.ts +6 -0
- package/dist/src/provider/stream.js +467 -3
- package/package.json +2 -2
|
@@ -31,3 +31,21 @@ export declare function messagesToCC(messages: PromptLike, options?: {
|
|
|
31
31
|
allowImages?: boolean;
|
|
32
32
|
}): unknown[];
|
|
33
33
|
export declare function systemPromptToText(value: unknown): string;
|
|
34
|
+
declare function openAITools(tools: unknown): unknown[] | undefined;
|
|
35
|
+
declare function anthropicTools(tools: unknown): unknown[] | undefined;
|
|
36
|
+
export interface ProviderRequestOptions {
|
|
37
|
+
prompt: PromptLike;
|
|
38
|
+
model?: string;
|
|
39
|
+
maxOutputTokens?: number;
|
|
40
|
+
providerOptions?: unknown;
|
|
41
|
+
tools?: unknown;
|
|
42
|
+
allowImages?: boolean;
|
|
43
|
+
systemPrompt?: unknown;
|
|
44
|
+
}
|
|
45
|
+
export declare function messagesToOpenAI(prompt: PromptLike, options?: Omit<ProviderRequestOptions, "prompt"> & {
|
|
46
|
+
prompt?: PromptLike;
|
|
47
|
+
}): Record<string, unknown>;
|
|
48
|
+
export declare function messagesToAnthropic(prompt: PromptLike, options?: Omit<ProviderRequestOptions, "prompt"> & {
|
|
49
|
+
prompt?: PromptLike;
|
|
50
|
+
}): Record<string, unknown>;
|
|
51
|
+
export { openAITools, anthropicTools };
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// src/provider/converters.ts — AI SDK v3 messages → Command Code payload (PLAN #2 Part B)
|
|
2
2
|
//
|
|
3
|
+
// Provider API shapes are documented at https://commandcode.ai/docs/provider:
|
|
4
|
+
// - POST /provider/v1/chat/completions follows OpenAI Chat Completions schema
|
|
5
|
+
// - POST /provider/v1/messages follows Anthropic Messages schema
|
|
6
|
+
// - Text + images only; audio/file/document rejected by schema (FAQ)
|
|
7
|
+
//
|
|
3
8
|
// Port of pi-commandcode-provider/src/converters.ts. Input is the AI SDK v3
|
|
4
9
|
// prompt format (LanguageModelV3Message / LanguageModelV3Prompt):
|
|
5
10
|
//
|
|
@@ -13,7 +18,9 @@
|
|
|
13
18
|
// shapes. `getApiKey` lives in ./auth-key.ts; `parseStreamEventLine` and
|
|
14
19
|
// `mapFinishReason` are issue #3.
|
|
15
20
|
import { toJsonSchema } from "./json-schema.js";
|
|
21
|
+
import { isReasoningModel, mappedReasoningEffort, resolveProviderReasoning, thinkingMetadataForModel, } from "./reasoning.js";
|
|
16
22
|
export { toJsonSchema } from "./json-schema.js";
|
|
23
|
+
const DEFAULT_PROVIDER_MAX_TOKENS = 64_000;
|
|
17
24
|
export function isRecord(value) {
|
|
18
25
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
26
|
}
|
|
@@ -45,13 +52,35 @@ export function numberValue(value) {
|
|
|
45
52
|
}
|
|
46
53
|
function imageParts(value) {
|
|
47
54
|
if (isRecord(value))
|
|
48
|
-
return value
|
|
49
|
-
return recordArray(value).filter((part) => part
|
|
50
|
-
(part.type === "file" && stringValue(part.mediaType)?.startsWith("image/")));
|
|
55
|
+
return isImageFilePart(value) ? [value] : [];
|
|
56
|
+
return recordArray(value).filter((part) => isImageFilePart(part));
|
|
51
57
|
}
|
|
52
58
|
function imageContentError(role) {
|
|
53
59
|
return new Error(`Selected Command Code model does not support image content in ${role}`);
|
|
54
60
|
}
|
|
61
|
+
function nonImageFileError(part, role) {
|
|
62
|
+
const mime = stringValue(part.mediaType) ?? stringValue(part.mimeType) ?? "unknown type";
|
|
63
|
+
return new Error(`Selected Command Code model only accepts text and images, but ${role} contain a non-image file (${mime}). Remove the attachment or convert it to an image.`);
|
|
64
|
+
}
|
|
65
|
+
function hasImageMediaType(part) {
|
|
66
|
+
const mime = stringValue(part.mediaType) ?? stringValue(part.mimeType);
|
|
67
|
+
// A `file` part must declare an image/* media type to be forwarded as an
|
|
68
|
+
// image; a `file` with no verifiable image type (and any non-image type) is
|
|
69
|
+
// rejected, since the Provider API accepts text + images only.
|
|
70
|
+
return mime !== undefined && mime.toLowerCase().startsWith("image/");
|
|
71
|
+
}
|
|
72
|
+
/** True for an `image` part or a `file` part carrying an image/* media type
|
|
73
|
+
* (mediaType or mimeType). Shared by the rejection guard and the content
|
|
74
|
+
* encoders so the image classification is consistent everywhere. */
|
|
75
|
+
function isImageFilePart(part) {
|
|
76
|
+
return part.type === "image" || (part.type === "file" && hasImageMediaType(part));
|
|
77
|
+
}
|
|
78
|
+
function assertProviderContentParts(parts, role) {
|
|
79
|
+
for (const part of parts) {
|
|
80
|
+
if (part.type === "file" && !hasImageMediaType(part))
|
|
81
|
+
throw nonImageFileError(part, role);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
55
84
|
export function assertTextOnlyMessages(messages) {
|
|
56
85
|
for (const message of messages ?? []) {
|
|
57
86
|
if (imageParts(message.content).length > 0) {
|
|
@@ -274,3 +303,435 @@ export function systemPromptToText(value) {
|
|
|
274
303
|
.join("\n\n");
|
|
275
304
|
return promptPartToText(value, 0);
|
|
276
305
|
}
|
|
306
|
+
function promptSystemText(prompt) {
|
|
307
|
+
const system = prompt.filter((m) => m.role === "system").map((m) => m.content);
|
|
308
|
+
return systemPromptToText(system.length > 0 ? system.join("\n") : undefined);
|
|
309
|
+
}
|
|
310
|
+
function cappedMaxTokens(value) {
|
|
311
|
+
const n = typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_PROVIDER_MAX_TOKENS;
|
|
312
|
+
return Math.min(n, DEFAULT_PROVIDER_MAX_TOKENS);
|
|
313
|
+
}
|
|
314
|
+
function reasoningEffortFor(providerOptions, modelId) {
|
|
315
|
+
try {
|
|
316
|
+
const effort = mappedReasoningEffort({
|
|
317
|
+
reasoning: modelId ? isReasoningModel(modelId) : true,
|
|
318
|
+
thinkingLevelMap: modelId ? thinkingMetadataForModel(modelId)?.thinkingLevelMap : undefined,
|
|
319
|
+
}, { reasoning: resolveProviderReasoning(providerOptions, "commandcode") });
|
|
320
|
+
return effort;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function dataToBase64(data) {
|
|
327
|
+
if (typeof data === "string")
|
|
328
|
+
return data;
|
|
329
|
+
if (data instanceof Uint8Array)
|
|
330
|
+
return Buffer.from(data).toString("base64");
|
|
331
|
+
if (ArrayBuffer.isView(data))
|
|
332
|
+
return Buffer.from(data).toString("base64");
|
|
333
|
+
if (data instanceof ArrayBuffer)
|
|
334
|
+
return Buffer.from(new Uint8Array(data)).toString("base64");
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
function toDataUrl(data, mediaType) {
|
|
338
|
+
const mime = stringValue(mediaType) ?? "application/octet-stream";
|
|
339
|
+
const raw = dataToBase64(data);
|
|
340
|
+
if (!raw || raw.length === 0)
|
|
341
|
+
return undefined;
|
|
342
|
+
if (raw.startsWith("data:"))
|
|
343
|
+
return raw;
|
|
344
|
+
return `data:${mime};base64,${raw}`;
|
|
345
|
+
}
|
|
346
|
+
function filePartToOpenAIImageUrl(part) {
|
|
347
|
+
const url = toDataUrl(part.data ?? part.image, part.mediaType ?? part.mimeType);
|
|
348
|
+
if (!url)
|
|
349
|
+
return undefined;
|
|
350
|
+
return { type: "image_url", image_url: { url } };
|
|
351
|
+
}
|
|
352
|
+
function filePartToAnthropicImage(part) {
|
|
353
|
+
const mime = stringValue(part.mediaType) ?? stringValue(part.mimeType) ?? "image/png";
|
|
354
|
+
let b64;
|
|
355
|
+
const raw = part.data ?? part.image;
|
|
356
|
+
if (typeof raw === "string") {
|
|
357
|
+
if (raw.startsWith("data:")) {
|
|
358
|
+
const after = raw.split(";base64,")[1];
|
|
359
|
+
b64 = after ?? raw;
|
|
360
|
+
const mimeFromUrl = raw.slice(5).split(";")[0];
|
|
361
|
+
if (mimeFromUrl) {
|
|
362
|
+
// prefer mime from data URL if present
|
|
363
|
+
return { type: "image", source: { type: "base64", media_type: mimeFromUrl, data: b64 } };
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
b64 = raw;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
b64 = dataToBase64(raw);
|
|
372
|
+
}
|
|
373
|
+
if (!b64)
|
|
374
|
+
return undefined;
|
|
375
|
+
return { type: "image", source: { type: "base64", media_type: mime, data: b64 } };
|
|
376
|
+
}
|
|
377
|
+
function textFromPart(part) {
|
|
378
|
+
return stringValue(part.text) ?? "";
|
|
379
|
+
}
|
|
380
|
+
function openAIUserContent(content, allowImages) {
|
|
381
|
+
if (typeof content === "string")
|
|
382
|
+
return content;
|
|
383
|
+
const parts = recordArray(content);
|
|
384
|
+
assertProviderContentParts(parts, "user messages");
|
|
385
|
+
const hasImages = parts.some(isImageFilePart);
|
|
386
|
+
if (!hasImages) {
|
|
387
|
+
const texts = parts.filter((p) => p.type === "text").map(textFromPart);
|
|
388
|
+
if (parts.every((p) => p.type === "text"))
|
|
389
|
+
return texts.join("\n");
|
|
390
|
+
const out = [];
|
|
391
|
+
for (const p of parts) {
|
|
392
|
+
if (p.type === "text")
|
|
393
|
+
out.push({ type: "text", text: textFromPart(p) });
|
|
394
|
+
else if (isImageFilePart(p)) {
|
|
395
|
+
if (!allowImages)
|
|
396
|
+
throw imageContentError("user messages");
|
|
397
|
+
const img = filePartToOpenAIImageUrl(p);
|
|
398
|
+
if (img)
|
|
399
|
+
out.push(img);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
if (!allowImages)
|
|
405
|
+
throw imageContentError("user messages");
|
|
406
|
+
const out = [];
|
|
407
|
+
for (const p of parts) {
|
|
408
|
+
if (p.type === "text")
|
|
409
|
+
out.push({ type: "text", text: textFromPart(p) });
|
|
410
|
+
else if (isImageFilePart(p)) {
|
|
411
|
+
const img = filePartToOpenAIImageUrl(p);
|
|
412
|
+
if (img)
|
|
413
|
+
out.push(img);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
function anthropicUserContent(content, allowImages) {
|
|
419
|
+
if (typeof content === "string")
|
|
420
|
+
return content;
|
|
421
|
+
const parts = recordArray(content);
|
|
422
|
+
assertProviderContentParts(parts, "user messages");
|
|
423
|
+
const hasImages = parts.some(isImageFilePart);
|
|
424
|
+
// Anthropic docs show string content for simple text: "Count to 5."
|
|
425
|
+
// Return string for single text-only part to match documented shape, array otherwise
|
|
426
|
+
if (!hasImages && parts.length === 1 && parts[0]?.type === "text") {
|
|
427
|
+
return textFromPart(parts[0]);
|
|
428
|
+
}
|
|
429
|
+
if (!hasImages && parts.every((p) => p.type === "text")) {
|
|
430
|
+
// Join multiple text parts with newline — provider accepts string for simple cases
|
|
431
|
+
const texts = parts.map((p) => textFromPart(p)).filter(Boolean);
|
|
432
|
+
if (texts.length === 1)
|
|
433
|
+
return texts[0];
|
|
434
|
+
// For multiple texts, keep array form to preserve structure
|
|
435
|
+
}
|
|
436
|
+
const out = [];
|
|
437
|
+
for (const p of parts) {
|
|
438
|
+
if (p.type === "text")
|
|
439
|
+
out.push({ type: "text", text: textFromPart(p) });
|
|
440
|
+
else if (isImageFilePart(p)) {
|
|
441
|
+
if (!allowImages)
|
|
442
|
+
throw imageContentError("user messages");
|
|
443
|
+
const img = filePartToAnthropicImage(p);
|
|
444
|
+
if (img)
|
|
445
|
+
out.push(img);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
function openAITools(tools) {
|
|
451
|
+
if (!tools)
|
|
452
|
+
return undefined;
|
|
453
|
+
let entries;
|
|
454
|
+
if (Array.isArray(tools)) {
|
|
455
|
+
entries = tools.map((t) => [
|
|
456
|
+
stringValue(t.name) ?? "",
|
|
457
|
+
t,
|
|
458
|
+
]);
|
|
459
|
+
}
|
|
460
|
+
else if (isRecord(tools)) {
|
|
461
|
+
entries = Object.entries(tools);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
if (entries.length === 0)
|
|
467
|
+
return undefined;
|
|
468
|
+
return entries.map(([name, tool]) => {
|
|
469
|
+
const rec = isRecord(tool) ? tool : {};
|
|
470
|
+
const description = stringValue(rec.description);
|
|
471
|
+
const parameters = rec.parameters ?? rec.inputSchema ?? {};
|
|
472
|
+
return {
|
|
473
|
+
type: "function",
|
|
474
|
+
function: {
|
|
475
|
+
name,
|
|
476
|
+
description,
|
|
477
|
+
parameters: toJsonSchema(parameters),
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function anthropicTools(tools) {
|
|
483
|
+
if (!tools)
|
|
484
|
+
return undefined;
|
|
485
|
+
let entries;
|
|
486
|
+
if (Array.isArray(tools)) {
|
|
487
|
+
entries = tools.map((t) => [
|
|
488
|
+
stringValue(t.name) ?? "",
|
|
489
|
+
t,
|
|
490
|
+
]);
|
|
491
|
+
}
|
|
492
|
+
else if (isRecord(tools)) {
|
|
493
|
+
entries = Object.entries(tools);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
return undefined;
|
|
497
|
+
}
|
|
498
|
+
if (entries.length === 0)
|
|
499
|
+
return undefined;
|
|
500
|
+
return entries.map(([name, tool]) => {
|
|
501
|
+
const rec = isRecord(tool) ? tool : {};
|
|
502
|
+
const description = stringValue(rec.description);
|
|
503
|
+
const parameters = rec.parameters ?? rec.inputSchema ?? {};
|
|
504
|
+
return {
|
|
505
|
+
name,
|
|
506
|
+
description,
|
|
507
|
+
input_schema: toJsonSchema(parameters),
|
|
508
|
+
};
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function promptToOpenAIMessages(prompt, allowImages) {
|
|
512
|
+
const out = [];
|
|
513
|
+
const system = promptSystemText(prompt);
|
|
514
|
+
if (system)
|
|
515
|
+
out.push({ role: "system", content: system });
|
|
516
|
+
const pairedIds = completeToolCallIds(prompt);
|
|
517
|
+
for (const message of prompt) {
|
|
518
|
+
if (message.role === "system")
|
|
519
|
+
continue;
|
|
520
|
+
if (message.role === "user") {
|
|
521
|
+
out.push({ role: "user", content: openAIUserContent(message.content, allowImages) });
|
|
522
|
+
}
|
|
523
|
+
else if (message.role === "assistant") {
|
|
524
|
+
const parts = recordArray(message.content);
|
|
525
|
+
const toolCalls = parts
|
|
526
|
+
.filter((p) => p.type === "tool-call")
|
|
527
|
+
.filter((p) => {
|
|
528
|
+
const id = stringValue(p.toolCallId);
|
|
529
|
+
return id ? pairedIds.has(id) : false;
|
|
530
|
+
});
|
|
531
|
+
const texts = parts
|
|
532
|
+
.filter((p) => p.type === "text")
|
|
533
|
+
.map(textFromPart)
|
|
534
|
+
.filter(Boolean);
|
|
535
|
+
if (toolCalls.length > 0) {
|
|
536
|
+
const content = texts.length > 0 ? texts.join("\n") : null;
|
|
537
|
+
out.push({
|
|
538
|
+
role: "assistant",
|
|
539
|
+
content,
|
|
540
|
+
tool_calls: toolCalls.map((p) => ({
|
|
541
|
+
id: stringValue(p.toolCallId) ?? "",
|
|
542
|
+
type: "function",
|
|
543
|
+
function: {
|
|
544
|
+
name: stringValue(p.toolName) ?? "",
|
|
545
|
+
arguments: JSON.stringify(recordOrEmpty(p.input ?? p.args ?? p.arguments)),
|
|
546
|
+
},
|
|
547
|
+
})),
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
else if (texts.length > 0) {
|
|
551
|
+
out.push({ role: "assistant", content: texts.join("\n") });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (message.role === "tool") {
|
|
555
|
+
const toolContent = recordArray(message.content);
|
|
556
|
+
assertProviderContentParts(toolContent, "tool results");
|
|
557
|
+
for (const content of toolContent) {
|
|
558
|
+
const toolCallId = stringValue(content.toolCallId) ?? "";
|
|
559
|
+
if (!pairedIds.has(toolCallId))
|
|
560
|
+
continue;
|
|
561
|
+
const output = unwrapToolResult(content.result ?? content.output);
|
|
562
|
+
out.push({ role: "tool", tool_call_id: toolCallId, content: output });
|
|
563
|
+
const images = imageParts(content);
|
|
564
|
+
if (images.length > 0) {
|
|
565
|
+
if (!allowImages)
|
|
566
|
+
throw imageContentError("tool results");
|
|
567
|
+
out.push({
|
|
568
|
+
role: "user",
|
|
569
|
+
content: images
|
|
570
|
+
.map((p) => filePartToOpenAIImageUrl(p))
|
|
571
|
+
.filter(Boolean),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
function promptToAnthropicMessages(prompt, allowImages) {
|
|
580
|
+
const out = [];
|
|
581
|
+
const pairedIds = completeToolCallIds(prompt);
|
|
582
|
+
for (const message of prompt) {
|
|
583
|
+
if (message.role === "system")
|
|
584
|
+
continue;
|
|
585
|
+
if (message.role === "user") {
|
|
586
|
+
out.push({ role: "user", content: anthropicUserContent(message.content, allowImages) });
|
|
587
|
+
}
|
|
588
|
+
else if (message.role === "assistant") {
|
|
589
|
+
const parts = recordArray(message.content);
|
|
590
|
+
const content = [];
|
|
591
|
+
for (const p of parts) {
|
|
592
|
+
if (p.type === "text") {
|
|
593
|
+
const text = textFromPart(p);
|
|
594
|
+
if (text)
|
|
595
|
+
content.push({ type: "text", text });
|
|
596
|
+
}
|
|
597
|
+
else if (p.type === "tool-call") {
|
|
598
|
+
const id = stringValue(p.toolCallId);
|
|
599
|
+
if (!id || !pairedIds.has(id))
|
|
600
|
+
continue;
|
|
601
|
+
content.push({
|
|
602
|
+
type: "tool_use",
|
|
603
|
+
id,
|
|
604
|
+
name: stringValue(p.toolName) ?? "",
|
|
605
|
+
input: recordOrEmpty(p.input ?? p.args ?? p.arguments),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (content.length > 0)
|
|
610
|
+
out.push({ role: "assistant", content });
|
|
611
|
+
}
|
|
612
|
+
else if (message.role === "tool") {
|
|
613
|
+
const toolContent = recordArray(message.content);
|
|
614
|
+
assertProviderContentParts(toolContent, "tool results");
|
|
615
|
+
for (const content of toolContent) {
|
|
616
|
+
const toolCallId = stringValue(content.toolCallId) ?? "";
|
|
617
|
+
if (!pairedIds.has(toolCallId))
|
|
618
|
+
continue;
|
|
619
|
+
const output = unwrapToolResult(content.result ?? content.output);
|
|
620
|
+
out.push({
|
|
621
|
+
role: "user",
|
|
622
|
+
content: [{ type: "tool_result", tool_use_id: toolCallId, content: output }],
|
|
623
|
+
});
|
|
624
|
+
const images = imageParts(content);
|
|
625
|
+
if (images.length > 0) {
|
|
626
|
+
if (!allowImages)
|
|
627
|
+
throw imageContentError("tool results");
|
|
628
|
+
out.push({
|
|
629
|
+
role: "user",
|
|
630
|
+
content: images
|
|
631
|
+
.map((p) => filePartToAnthropicImage(p))
|
|
632
|
+
.filter(Boolean),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return out;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* OpenAI Chat Completions body for POST /provider/v1/chat/completions.
|
|
642
|
+
* Docs: https://commandcode.ai/docs/provider#quickstart / #streaming — follows
|
|
643
|
+
* OpenAI schema; streaming example shows stream:true + stream_options:{include_usage:true}.
|
|
644
|
+
* Provider FAQ: text + images only; max_tokens honoured for both transports (capped 64k).
|
|
645
|
+
* reasoning_effort is CLI extension via reasoning.ts (not in provider docs), byte-equivalent to shared helpers.
|
|
646
|
+
*/
|
|
647
|
+
function buildOpenAIBody(options) {
|
|
648
|
+
const prompt = options.prompt ?? [];
|
|
649
|
+
const allowImages = options.allowImages ?? false;
|
|
650
|
+
if (!allowImages)
|
|
651
|
+
assertTextOnlyMessages(prompt);
|
|
652
|
+
const messages = promptToOpenAIMessages(prompt, allowImages);
|
|
653
|
+
const body = {
|
|
654
|
+
model: options.model ?? "",
|
|
655
|
+
stream: true,
|
|
656
|
+
messages,
|
|
657
|
+
};
|
|
658
|
+
if (options.tools) {
|
|
659
|
+
const t = openAITools(options.tools);
|
|
660
|
+
if (t)
|
|
661
|
+
body.tools = t;
|
|
662
|
+
}
|
|
663
|
+
// Anthropic requires max_tokens; for OpenAI we set capped default to honour #52
|
|
664
|
+
// (byte-equivalent to shared cappedMaxTokens). Provider models list shows context_length up to 1M
|
|
665
|
+
// but transport caps at DEFAULT_PROVIDER_MAX_TOKENS.
|
|
666
|
+
const maxTokens = cappedMaxTokens(options.maxOutputTokens);
|
|
667
|
+
body.max_tokens = maxTokens;
|
|
668
|
+
// System is already inside messages for OpenAI, but also accept explicit systemPrompt
|
|
669
|
+
const explicitSystem = systemPromptToText(options.systemPrompt);
|
|
670
|
+
if (explicitSystem && !prompt.some((m) => m.role === "system")) {
|
|
671
|
+
body.messages = [{ role: "system", content: explicitSystem }, ...messages];
|
|
672
|
+
}
|
|
673
|
+
const effort = reasoningEffortFor(options.providerOptions, options.model);
|
|
674
|
+
if (effort)
|
|
675
|
+
body.reasoning_effort = effort;
|
|
676
|
+
// OpenAI streaming usage needs stream_options — doc streaming example includes it
|
|
677
|
+
body.stream_options = { include_usage: true };
|
|
678
|
+
return body;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Anthropic Messages body for POST /provider/v1/messages.
|
|
682
|
+
* Docs: https://commandcode.ai/docs/provider — follows Anthropic schema; system is
|
|
683
|
+
* top-level string (not in messages), stream:true, max_tokens required (we default/cap 64k).
|
|
684
|
+
* reasoning_effort same CLI extension as OpenAI path.
|
|
685
|
+
*/
|
|
686
|
+
function buildAnthropicBody(options) {
|
|
687
|
+
const prompt = options.prompt ?? [];
|
|
688
|
+
const allowImages = options.allowImages ?? false;
|
|
689
|
+
if (!allowImages)
|
|
690
|
+
assertTextOnlyMessages(prompt);
|
|
691
|
+
const system = promptSystemText(prompt) || systemPromptToText(options.systemPrompt);
|
|
692
|
+
const messages = promptToAnthropicMessages(prompt, allowImages);
|
|
693
|
+
const body = {
|
|
694
|
+
model: options.model ?? "",
|
|
695
|
+
stream: true,
|
|
696
|
+
messages,
|
|
697
|
+
};
|
|
698
|
+
if (system)
|
|
699
|
+
body.system = system;
|
|
700
|
+
if (options.tools) {
|
|
701
|
+
const t = anthropicTools(options.tools);
|
|
702
|
+
if (t)
|
|
703
|
+
body.tools = t;
|
|
704
|
+
}
|
|
705
|
+
const maxTokens = cappedMaxTokens(options.maxOutputTokens);
|
|
706
|
+
body.max_tokens = maxTokens;
|
|
707
|
+
const effort = reasoningEffortFor(options.providerOptions, options.model);
|
|
708
|
+
if (effort)
|
|
709
|
+
body.reasoning_effort = effort;
|
|
710
|
+
return body;
|
|
711
|
+
}
|
|
712
|
+
// Primary codec helpers — naming follows spec sketch but aliases are provided
|
|
713
|
+
export function messagesToOpenAI(prompt, options = {}) {
|
|
714
|
+
const p = prompt ?? options.prompt ?? [];
|
|
715
|
+
// Support both call shapes: messagesToOpenAI(prompt, opts) and messagesToOpenAI({prompt, model, ...})
|
|
716
|
+
if (Array.isArray(prompt) && isRecord(options) && "prompt" in options) {
|
|
717
|
+
// Called as messagesToOpenAI({ prompt, model, ... })
|
|
718
|
+
return buildOpenAIBody(options);
|
|
719
|
+
}
|
|
720
|
+
if (Array.isArray(prompt)) {
|
|
721
|
+
return buildOpenAIBody({ prompt: p, ...options });
|
|
722
|
+
}
|
|
723
|
+
// Called as messagesToOpenAI(optionsObject)
|
|
724
|
+
return buildOpenAIBody(prompt);
|
|
725
|
+
}
|
|
726
|
+
export function messagesToAnthropic(prompt, options = {}) {
|
|
727
|
+
const p = prompt ?? options.prompt ?? [];
|
|
728
|
+
if (Array.isArray(prompt) && isRecord(options) && "prompt" in options) {
|
|
729
|
+
return buildAnthropicBody(options);
|
|
730
|
+
}
|
|
731
|
+
if (Array.isArray(prompt)) {
|
|
732
|
+
return buildAnthropicBody({ prompt: p, ...options });
|
|
733
|
+
}
|
|
734
|
+
return buildAnthropicBody(prompt);
|
|
735
|
+
}
|
|
736
|
+
// Canonical codec entry points: messagesToOpenAI / messagesToAnthropic.
|
|
737
|
+
export { openAITools, anthropicTools };
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
export declare function redactCommandCodeErrorText(value: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Detects the documented Provider API `403 upgrade_required` signal — "You're
|
|
4
|
+
* on the Go plan, the only plan without API access. Upgrade to GOAT or
|
|
5
|
+
* higher." (https://commandcode.ai/docs/provider). Tolerant to the documented
|
|
6
|
+
* variants: the JSON `error.code` / `error.type` may be `upgrade_required` and
|
|
7
|
+
* the message may phrase the same upgrade intent ("without API access",
|
|
8
|
+
* "Upgrade to GOAT"). A 403 that merely mentions the Go plan without that
|
|
9
|
+
* intent (e.g. "forbidden") is not a flip signal, and any status other than
|
|
10
|
+
* 403 (401, 422 cmd_zdr_no_providers, 429, 5xx, ...) never is either.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isUpgradeRequiredError(status: number, body: unknown): boolean;
|
|
2
13
|
export declare function commandCodeErrorMessage(value: unknown): string | undefined;
|
|
@@ -26,6 +26,44 @@ export function redactCommandCodeErrorText(value) {
|
|
|
26
26
|
function isRecord(value) {
|
|
27
27
|
return typeof value === "object" && value !== null;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Detects the documented Provider API `403 upgrade_required` signal — "You're
|
|
31
|
+
* on the Go plan, the only plan without API access. Upgrade to GOAT or
|
|
32
|
+
* higher." (https://commandcode.ai/docs/provider). Tolerant to the documented
|
|
33
|
+
* variants: the JSON `error.code` / `error.type` may be `upgrade_required` and
|
|
34
|
+
* the message may phrase the same upgrade intent ("without API access",
|
|
35
|
+
* "Upgrade to GOAT"). A 403 that merely mentions the Go plan without that
|
|
36
|
+
* intent (e.g. "forbidden") is not a flip signal, and any status other than
|
|
37
|
+
* 403 (401, 422 cmd_zdr_no_providers, 429, 5xx, ...) never is either.
|
|
38
|
+
*/
|
|
39
|
+
export function isUpgradeRequiredError(status, body) {
|
|
40
|
+
if (status !== 403)
|
|
41
|
+
return false;
|
|
42
|
+
const candidates = [];
|
|
43
|
+
const pushStrings = (record) => {
|
|
44
|
+
for (const key of ["code", "type", "message"]) {
|
|
45
|
+
const part = record[key];
|
|
46
|
+
if (typeof part === "string")
|
|
47
|
+
candidates.push(part);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
if (typeof body === "string") {
|
|
51
|
+
candidates.push(body);
|
|
52
|
+
try {
|
|
53
|
+
body = JSON.parse(body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// keep the raw text as the only candidate below
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (isRecord(body)) {
|
|
60
|
+
const error = body.error;
|
|
61
|
+
if (isRecord(error))
|
|
62
|
+
pushStrings(error);
|
|
63
|
+
pushStrings(body);
|
|
64
|
+
}
|
|
65
|
+
return candidates.some((c) => /upgrade_required|upgrade to goat|without api access/i.test(c));
|
|
66
|
+
}
|
|
29
67
|
export function commandCodeErrorMessage(value) {
|
|
30
68
|
if (typeof value === "string")
|
|
31
69
|
return value;
|
|
@@ -3,3 +3,9 @@ export declare function parseStreamEventLine(line: string): unknown | undefined;
|
|
|
3
3
|
export declare function mapFinishReason(reason: unknown): LanguageModelV3FinishReason;
|
|
4
4
|
export declare function ccUsageToAiSdkUsage(event: Record<string, unknown>): LanguageModelV3Usage | undefined;
|
|
5
5
|
export declare function ccEventToStreamPart(event: unknown): LanguageModelV3StreamPart[];
|
|
6
|
+
export declare function openAIUsageToAiSdkUsage(event: Record<string, unknown>): LanguageModelV3Usage | undefined;
|
|
7
|
+
export declare function anthropicUsageToAiSdkUsage(event: Record<string, unknown>): LanguageModelV3Usage | undefined;
|
|
8
|
+
export declare function openAIEventToStreamPart(event: unknown): LanguageModelV3StreamPart[];
|
|
9
|
+
export declare function anthropicEventToStreamPart(event: unknown): LanguageModelV3StreamPart[];
|
|
10
|
+
export declare function createOpenAIStreamParser(): (event: unknown) => LanguageModelV3StreamPart[];
|
|
11
|
+
export declare function createAnthropicStreamParser(): (event: unknown) => LanguageModelV3StreamPart[];
|