opencode-cmd-provider 1.4.0 → 1.5.1

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 CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.1 - 2026-08-24
4
+
5
+ Fix: provider parsers now synthesize the full reasoning and text lifecycle —
6
+ `reasoning-start`/`reasoning-end` and `text-start`/`text-end` — for both
7
+ OpenAI-style and Anthropic-style streams, so AI SDK consumers see balanced
8
+ section boundaries instead of missing or duplicated end events.
9
+
10
+ ### Fixes
11
+
12
+ - **OpenAI-style streams** now emit `reasoning-start` before the first
13
+ reasoning delta and `reasoning-end` before text, tool calls, or finish; the
14
+ same for `text-start`/`text-end`. Reasoning and text sections use stable ids
15
+ from the first chunk (instead of per-chunk ids), and unfinished sections are
16
+ closed when the stream finishes.
17
+ - **Anthropic-style streams** now recognize `thinking` blocks and emit
18
+ `reasoning-start`/`reasoning-delta`/`reasoning-end` for them. `content_block_stop`
19
+ closes each block with the correct end event per its type (`text-end`,
20
+ `reasoning-end`, or `tool-input-end` + a single `tool-call`), replacing the
21
+ previous "emit both text-end and tool-input-end" pair that left consumers to
22
+ ignore the spurious one.
23
+ - New `tests/stream.test.ts` cases cover the OpenAI reasoning→text ordering,
24
+ reasoning-only finishes, and the Anthropic thinking-block lifecycle;
25
+ `tests/provider-parity.test.ts` accepts `text-start` as the first text part.
26
+
27
+ ## 1.5.0 - 2026-08-23
28
+
29
+ Feature: the `[CMD] ` display-name prefix for auto-registered models is now
30
+ configurable via `provider.commandcode.options.display_prefix`.
31
+
32
+ ### Features
33
+
34
+ - **Configurable display-name prefix** (issue #60): a string value replaces the
35
+ default `[CMD] ` prefix, and an empty string disables it entirely. The option
36
+ is read-only at resolution time — nothing is persisted into the user's config
37
+ (unlike `npm`/`name`/`env`/`options.baseURL`, which are filled when unset).
38
+ - Non-string values fall back to the default `[CMD] ` prefix.
39
+ - Declared model entries are never renamed — the prefix applies only to models
40
+ auto-registered from the bundled snapshot.
41
+
42
+ ### Chores
43
+
44
+ - New `tests/plugin-models.test.ts` cases: `resolveDisplayPrefix` fallback,
45
+ prefix override (`"CC/"`), empty string disabling the prefix (with non-name
46
+ metadata unaffected), and declared models keeping their names regardless of
47
+ the setting.
48
+
3
49
  ## 1.4.0 - 2026-08-23
4
50
 
5
51
  Feature: after a successful `/connect`, the credential is mirrored under
package/README.md CHANGED
@@ -133,6 +133,24 @@ can still declare your own `provider.commandcode` entry; your declarations
133
133
  always win and the snapshot fills in only what's missing (`whitelist`/
134
134
  `blacklist` on a declared entry filter the auto-registered models too).
135
135
 
136
+ The `[CMD] ` display-name prefix is configurable through the declared
137
+ provider entry's options:
138
+
139
+ ```jsonc
140
+ {
141
+ "provider": {
142
+ "commandcode": {
143
+ "options": {
144
+ "display_prefix": "", // default "[CMD] "; empty string disables
145
+ },
146
+ },
147
+ },
148
+ }
149
+ ```
150
+
151
+ Declared model entries are never renamed; the prefix applies to
152
+ auto-registered models only.
153
+
136
154
  - Snapshot — `src/catalog/snapshot.ts` (model ids, names, context lengths) plus
137
155
  `src/catalog/facts.ts` (reasoning efforts, per-1M-token rates, input
138
156
  modalities). Regenerated from the live catalog via `npm run refresh:snapshot`.
@@ -1,7 +1,7 @@
1
1
  export declare const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/bundled/command-code-knowledge/reference/models.md";
2
2
  export declare const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/cli.mjs";
3
3
  export declare const FACTS_PACKAGE_VERSION = "1.32.1";
4
- export declare const FACTS_LAST_REFRESHED = "2026-08-22";
4
+ export declare const FACTS_LAST_REFRESHED = "2026-08-24";
5
5
  export declare const MODEL_EFFORTS: Readonly<Record<string, readonly string[]>>;
6
6
  export declare const MODEL_COSTS: Readonly<Record<string, {
7
7
  input: number;
@@ -7,7 +7,7 @@
7
7
  export const FACTS_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/bundled/command-code-knowledge/reference/models.md";
8
8
  export const MODALITIES_SOURCE_URL = "https://unpkg.com/command-code@1.32.1/dist/cli.mjs";
9
9
  export const FACTS_PACKAGE_VERSION = "1.32.1";
10
- export const FACTS_LAST_REFRESHED = "2026-08-22";
10
+ export const FACTS_LAST_REFRESHED = "2026-08-24";
11
11
  export const MODEL_EFFORTS = {
12
12
  "deepseek/deepseek-v4-pro": ["high", "max"],
13
13
  "deepseek/deepseek-v4-flash": ["high", "max"],
@@ -51,5 +51,5 @@ export interface ModelDeals {
51
51
  export declare const MODEL_DEALS: Readonly<Record<string, ModelDeals>>;
52
52
  export declare const PLAN_CATALOG: Readonly<Record<PlanId, PlanInfo>>;
53
53
  export declare const DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
54
- export declare const DEAL_LAST_REFRESHED = "2026-08-22";
54
+ export declare const DEAL_LAST_REFRESHED = "2026-08-24";
55
55
  export declare const DEAL_PACKAGE_VERSION = "docs";
@@ -74,5 +74,5 @@ export const PLAN_CATALOG = {
74
74
  provider: { price: 15, credits: 0, window5h: 0, windowWeek: 0, display: "Provider" },
75
75
  };
76
76
  export const DEAL_SOURCE_URL = "https://commandcode.ai/docs/resources/pricing-limits";
77
- export const DEAL_LAST_REFRESHED = "2026-08-22";
77
+ export const DEAL_LAST_REFRESHED = "2026-08-24";
78
78
  export const DEAL_PACKAGE_VERSION = "docs";
@@ -1,5 +1,13 @@
1
- import type { Config } from "@opencode-ai/sdk/v2";
1
+ import type { Config, ProviderConfig } from "@opencode-ai/sdk/v2";
2
2
  import type { CatalogModel } from "../catalog/snapshot.js";
3
+ export declare const DEFAULT_DISPLAY_PREFIX = "[CMD] ";
4
+ /**
5
+ * Resolves the auto-registration display-name prefix from the user-declared
6
+ * `provider.commandcode.options.display_prefix` key. A string value is used
7
+ * verbatim (empty string disables the prefix); anything else falls back to
8
+ * `[CMD] `. Read-only: never persisted into the user's config.
9
+ */
10
+ export declare function resolveDisplayPrefix(entry: ProviderConfig | undefined): string;
3
11
  export interface AutoRegisterOptions {
4
12
  npm: string;
5
13
  name: string;
@@ -24,6 +32,9 @@ export interface AutoRegisterOptions {
24
32
  * config key or by the entry's `id`);
25
33
  * - declared models are never modified, and declared models that left the
26
34
  * catalog stay usable.
35
+ * - `options.display_prefix` (string) overrides the default `[CMD] `
36
+ * display-name prefix for auto-registered models; an empty string disables
37
+ * the prefix.
27
38
  */
28
39
  export declare function autoRegister(config: Config, snapshot: readonly CatalogModel[], options: AutoRegisterOptions): Config;
29
40
  /**
@@ -2,6 +2,17 @@ import { MODEL_COSTS, ZERO_MODEL_COST } from "../provider/pricing.js";
2
2
  import { reasoningVariantsForModel, isReasoningModel } from "../provider/reasoning.js";
3
3
  import { inputModalitiesForModel } from "../provider/modalities.js";
4
4
  const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
5
+ export const DEFAULT_DISPLAY_PREFIX = "[CMD] ";
6
+ /**
7
+ * Resolves the auto-registration display-name prefix from the user-declared
8
+ * `provider.commandcode.options.display_prefix` key. A string value is used
9
+ * verbatim (empty string disables the prefix); anything else falls back to
10
+ * `[CMD] `. Read-only: never persisted into the user's config.
11
+ */
12
+ export function resolveDisplayPrefix(entry) {
13
+ const value = entry?.options?.display_prefix;
14
+ return typeof value === "string" ? value : DEFAULT_DISPLAY_PREFIX;
15
+ }
5
16
  /**
6
17
  * Registers the `commandcode` provider entry into opencode's config so every
7
18
  * snapshot model is available without any user declaration.
@@ -21,6 +32,9 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 65_536;
21
32
  * config key or by the entry's `id`);
22
33
  * - declared models are never modified, and declared models that left the
23
34
  * catalog stay usable.
35
+ * - `options.display_prefix` (string) overrides the default `[CMD] `
36
+ * display-name prefix for auto-registered models; an empty string disables
37
+ * the prefix.
24
38
  */
25
39
  export function autoRegister(config, snapshot, options) {
26
40
  if (snapshot.length === 0)
@@ -33,13 +47,14 @@ export function autoRegister(config, snapshot, options) {
33
47
  entry.options ??= {};
34
48
  entry.options.baseURL ??= options.baseURL;
35
49
  entry.models ??= {};
50
+ const prefix = resolveDisplayPrefix(entry);
36
51
  const declaredById = new Set(Object.values(entry.models)
37
52
  .map((model) => model?.id)
38
53
  .filter((id) => typeof id === "string"));
39
54
  for (const model of snapshot) {
40
55
  if (entry.models[model.id] !== undefined || declaredById.has(model.id))
41
56
  continue;
42
- entry.models[model.id] = configModelFor(model);
57
+ entry.models[model.id] = configModelFor(model, prefix);
43
58
  }
44
59
  return config;
45
60
  }
@@ -73,15 +88,15 @@ export function augmentConfigCommandCodeModels(config) {
73
88
  }
74
89
  /**
75
90
  * Config-schema model entry (not the SDK `Model` shape) for a snapshot model:
76
- * `[CMD]`-prefixed display name, context/output limits with output capped at
91
+ * prefixed display name, context/output limits with output capped at
77
92
  * 65_536, and metadata enriched from the reasoning, modality, and pricing
78
93
  * tables.
79
94
  */
80
- function configModelFor(model) {
95
+ function configModelFor(model, prefix = DEFAULT_DISPLAY_PREFIX) {
81
96
  const variants = reasoningVariantsForModel(model.id);
82
97
  const costs = MODEL_COSTS[model.id] ?? ZERO_MODEL_COST;
83
98
  return {
84
- name: `[CMD] ${model.name}`,
99
+ name: `${prefix}${model.name}`,
85
100
  limit: {
86
101
  context: model.contextLength,
87
102
  output: Math.min(model.contextLength, DEFAULT_MAX_OUTPUT_TOKENS),
@@ -338,6 +338,10 @@ export function anthropicEventToStreamPart(event) {
338
338
  const id = `text-${index}`;
339
339
  return [{ type: "text-start", id }];
340
340
  }
341
+ if (blockType === "thinking") {
342
+ const id = stringValue(block?.id) ?? `thinking-${index}`;
343
+ return [{ type: "reasoning-start", id }];
344
+ }
341
345
  if (blockType === "tool_use") {
342
346
  const id = stringValue(block?.id) ?? `tool-${index}`;
343
347
  const name = stringValue(block?.name) ?? "";
@@ -345,16 +349,32 @@ export function anthropicEventToStreamPart(event) {
345
349
  }
346
350
  return [];
347
351
  }
352
+ if (type === "content_block_delta") {
353
+ const delta = asRecord(event.delta);
354
+ const index = numberValue(event.index) ?? 0;
355
+ const text = stringValue(delta?.text);
356
+ if (typeof text === "string" && text.length > 0) {
357
+ const id = `text-${index}`;
358
+ return [{ type: "text-delta", id, delta: text }];
359
+ }
360
+ const thinking = stringValue(delta?.thinking);
361
+ if (typeof thinking === "string" && thinking.length > 0) {
362
+ const id = `thinking-${index}`;
363
+ return [{ type: "reasoning-delta", id, delta: thinking }];
364
+ }
365
+ // Tool input delta: { type: "input_json_delta", partial_json: "..." }
366
+ const partial = stringValue(delta?.partial_json);
367
+ if (typeof partial === "string" && partial.length > 0) {
368
+ const id = `tool-${index}`;
369
+ // Emit as tool-input-delta; caller may have started tool
370
+ return [{ type: "tool-input-delta", id, delta: partial }];
371
+ }
372
+ return [];
373
+ }
348
374
  if (type === "content_block_stop") {
349
375
  // Stateless codec: the STOP event carries only `index`, not the block type.
350
- // Anthropic streams either `text` or `tool_use` blocks; the AI SDK expects
351
- // `text-end` for the former and `tool-input-end` for the latter. Emitting
352
- // both is noisy (previous emitted two parts) but guarantees the right end
353
- // is seen regardless of block type, and the consumer ignores the spurious
354
- // one per AI SDK spec. Emit a single text-end here would break tool_use
355
- // streams that rely on tool-input-end, so we keep the conservative pair
356
- // with an explicit note referencing provider doc streaming (message_delta
357
- // carries the final usage/finish; per-block ends are AI SDK concerns).
376
+ // Anthropic streams either `text`, `thinking` or `tool_use` blocks; the AI SDK expects
377
+ // `text-end` for text, `reasoning-end` for thinking, and `tool-input-end` for tool_use.
358
378
  const index = numberValue(event.index) ?? 0;
359
379
  const idText = `text-${index}`;
360
380
  const idTool = `tool-${index}`;
@@ -395,6 +415,26 @@ export function createOpenAIStreamParser() {
395
415
  // finish_reason here so the usage-only chunk's finish keeps the real reason
396
416
  // (e.g. "length") instead of defaulting to "stop".
397
417
  let lastFinishReason;
418
+ let reasoningStarted = false;
419
+ let reasoningEnded = false;
420
+ let reasoningId;
421
+ let textStarted = false;
422
+ let textEnded = false;
423
+ let textId;
424
+ function closeReasoning() {
425
+ if (reasoningStarted && !reasoningEnded && reasoningId) {
426
+ reasoningEnded = true;
427
+ return [{ type: "reasoning-end", id: reasoningId }];
428
+ }
429
+ return [];
430
+ }
431
+ function closeText() {
432
+ if (textStarted && !textEnded && textId) {
433
+ textEnded = true;
434
+ return [{ type: "text-end", id: textId }];
435
+ }
436
+ return [];
437
+ }
398
438
  return (event) => {
399
439
  if (!isRecord(event))
400
440
  return [];
@@ -405,17 +445,42 @@ export function createOpenAIStreamParser() {
405
445
  const parts = [];
406
446
  const choice = firstChoice(event);
407
447
  const delta = choice ? deltaFromChoice(choice) : undefined;
448
+ const eventId = stringValue(event.id);
408
449
  if (delta) {
409
- const content = stringValue(delta.content);
410
- if (typeof content === "string" && content.length > 0) {
411
- parts.push(textDeltaPart(event.id, content));
412
- }
413
450
  const reasoning = stringValue(delta.reasoning) ?? stringValue(delta.reasoning_content);
414
451
  if (typeof reasoning === "string" && reasoning.length > 0) {
415
- parts.push(reasoningDeltaPart(event.id, reasoning));
452
+ if (!reasoningStarted || reasoningEnded) {
453
+ if (textStarted && !textEnded) {
454
+ parts.push(...closeText());
455
+ }
456
+ reasoningStarted = true;
457
+ reasoningEnded = false;
458
+ reasoningId = eventId || "reasoning-0";
459
+ parts.push({ type: "reasoning-start", id: reasoningId });
460
+ }
461
+ parts.push(reasoningDeltaPart(reasoningId ?? eventId, reasoning));
462
+ }
463
+ const content = stringValue(delta.content);
464
+ if (typeof content === "string" && content.length > 0) {
465
+ if (reasoningStarted && !reasoningEnded) {
466
+ parts.push(...closeReasoning());
467
+ }
468
+ if (!textStarted || textEnded) {
469
+ textStarted = true;
470
+ textEnded = false;
471
+ textId = eventId || "text-0";
472
+ parts.push({ type: "text-start", id: textId });
473
+ }
474
+ parts.push(textDeltaPart(textId ?? eventId, content));
416
475
  }
417
476
  const toolCalls = delta.tool_calls ?? delta.toolCalls;
418
477
  if (Array.isArray(toolCalls)) {
478
+ if (reasoningStarted && !reasoningEnded) {
479
+ parts.push(...closeReasoning());
480
+ }
481
+ if (textStarted && !textEnded) {
482
+ parts.push(...closeText());
483
+ }
419
484
  for (const tc of toolCalls) {
420
485
  if (!isRecord(tc))
421
486
  continue;
@@ -475,6 +540,12 @@ export function createOpenAIStreamParser() {
475
540
  if (finishReason)
476
541
  lastFinishReason = finishReason;
477
542
  if (lastFinishReason || hasUsage) {
543
+ if (reasoningStarted && !reasoningEnded) {
544
+ parts.push(...closeReasoning());
545
+ }
546
+ if (textStarted && !textEnded) {
547
+ parts.push(...closeText());
548
+ }
478
549
  // Flush any tool call whose terminal args chunk never arrived.
479
550
  for (const [index, buffer] of toolBuffers) {
480
551
  if (buffer.started && !buffer.emitted) {
@@ -498,6 +569,7 @@ export function createOpenAIStreamParser() {
498
569
  }
499
570
  export function createAnthropicStreamParser() {
500
571
  const toolBlocks = new Map();
572
+ const blockTypes = new Map();
501
573
  return (event) => {
502
574
  if (!isRecord(event))
503
575
  return [];
@@ -507,13 +579,21 @@ export function createAnthropicStreamParser() {
507
579
  const index = numberValue(event.index) ?? 0;
508
580
  const blockType = stringValue(block?.type);
509
581
  if (blockType === "tool_use") {
582
+ blockTypes.set(index, "tool_use");
510
583
  const id = stringValue(block?.id) ?? `tool-${index}`;
511
584
  const name = stringValue(block?.name) ?? "";
512
585
  toolBlocks.set(index, { id, name, input: "", started: true, emitted: false });
513
586
  return [{ type: "tool-input-start", id, toolName: name }];
514
587
  }
515
- if (blockType === "text")
588
+ if (blockType === "thinking") {
589
+ blockTypes.set(index, "thinking");
590
+ const id = stringValue(block?.id) ?? `thinking-${index}`;
591
+ return [{ type: "reasoning-start", id }];
592
+ }
593
+ if (blockType === "text") {
594
+ blockTypes.set(index, "text");
516
595
  return [{ type: "text-start", id: `text-${index}` }];
596
+ }
517
597
  return [];
518
598
  }
519
599
  if (type === "content_block_delta") {
@@ -523,6 +603,10 @@ export function createAnthropicStreamParser() {
523
603
  if (typeof text === "string" && text.length > 0) {
524
604
  return [{ type: "text-delta", id: `text-${index}`, delta: text }];
525
605
  }
606
+ const thinking = stringValue(delta?.thinking);
607
+ if (typeof thinking === "string" && thinking.length > 0) {
608
+ return [{ type: "reasoning-delta", id: `thinking-${index}`, delta: thinking }];
609
+ }
526
610
  const partial = stringValue(delta?.partial_json);
527
611
  if (typeof partial === "string" && partial.length > 0) {
528
612
  const block = toolBlocks.get(index);
@@ -557,6 +641,28 @@ export function createAnthropicStreamParser() {
557
641
  }
558
642
  if (type === "content_block_stop") {
559
643
  const index = numberValue(event.index) ?? 0;
644
+ const bType = blockTypes.get(index);
645
+ blockTypes.delete(index);
646
+ if (bType === "tool_use") {
647
+ const block = toolBlocks.get(index);
648
+ if (block) {
649
+ toolBlocks.delete(index);
650
+ if (block.emitted)
651
+ return [];
652
+ return [
653
+ { type: "tool-input-end", id: block.id },
654
+ {
655
+ type: "tool-call",
656
+ toolCallId: block.id,
657
+ toolName: block.name,
658
+ input: block.input,
659
+ },
660
+ ];
661
+ }
662
+ }
663
+ else if (bType === "thinking") {
664
+ return [{ type: "reasoning-end", id: `thinking-${index}` }];
665
+ }
560
666
  const block = toolBlocks.get(index);
561
667
  if (block) {
562
668
  toolBlocks.delete(index);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-cmd-provider",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Command Code provider + plugin for opencode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",