@bogyie/opencode-kiro-plugin 0.2.1 → 0.2.3

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 CHANGED
@@ -10,16 +10,7 @@ After the package is published to npm, add it directly to your OpenCode config:
10
10
 
11
11
  ```jsonc
12
12
  {
13
- "plugin": ["@bogyie/opencode-kiro-plugin"],
14
- "provider": {
15
- "kiro": {
16
- "models": {
17
- "sonnet": {
18
- "name": "Sonnet alias"
19
- }
20
- }
21
- }
22
- }
13
+ "plugin": ["@bogyie/opencode-kiro-plugin"]
23
14
  }
24
15
  ```
25
16
 
@@ -34,16 +25,7 @@ Then add the plugin to your OpenCode config. See [examples/opencode.jsonc](examp
34
25
 
35
26
  ```jsonc
36
27
  {
37
- "plugin": ["file:/absolute/path/to/opencode-kiro-plugin"],
38
- "provider": {
39
- "kiro": {
40
- "models": {
41
- "sonnet": {
42
- "name": "Sonnet alias"
43
- }
44
- }
45
- }
46
- }
28
+ "plugin": ["file:/absolute/path/to/opencode-kiro-plugin"]
47
29
  }
48
30
  ```
49
31
 
@@ -85,14 +67,14 @@ Supported values:
85
67
 
86
68
  - `auto`: use CodeWhisperer fetch transport when an API key is available; otherwise use CLI chat fallback.
87
69
  - `fetch`: require the direct Kiro/CodeWhisperer fetch path. If no usable auth is available, requests fail with a structured backend/auth error.
88
- - `cli-chat`: call `kiro-cli chat --no-interactive`. This is official and stable, but Kiro CLI does not currently expose a guaranteed per-request model flag.
70
+ - `cli-chat`: call `kiro-cli chat --no-interactive --model <model>`. This is official and stable, and uses the model selected in OpenCode.
89
71
  - `acp`: launch `kiro-cli acp`, initialize a session, optionally set the requested model, send the prompt, and collect `AgentMessageChunk` notifications until `TurnEnd`.
90
72
 
91
73
  `trustAllTools` affects both `cli-chat` and ACP permission handling. In ACP mode, permission requests are rejected by default and allowed only when `trustAllTools: true`.
92
74
 
93
75
  ## Model Churn Handling
94
76
 
95
- The resolver intentionally avoids a hard whitelist. Fallback presets are used for OpenCode UI metadata and cache bootstrap only.
77
+ The resolver intentionally avoids a hard whitelist. By default, the plugin reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json`. Bundled presets are only used as UI metadata and as a fallback when discovery is unavailable.
96
78
 
97
79
  Useful options:
98
80
 
@@ -104,13 +86,13 @@ Useful options:
104
86
  {
105
87
  "modelCacheTtlSeconds": 21600,
106
88
  "modelDiscovery": "auto",
107
- "modelDiscoveryCommand": ["kiro-cli", "models", "--json"],
89
+ "modelDiscoveryCommand": ["kiro-cli", "chat", "--list-models", "--format", "json"],
108
90
  "modelAliases": {
109
- "sonnet": "claude-sonnet-4.6",
110
- "opus": "claude-opus-4.6"
91
+ "sonnet": "claude-sonnet-5",
92
+ "opus": "claude-opus-4.8"
111
93
  },
112
94
  "extraModels": {
113
- "claude-opus-4-9": {
95
+ "claude-opus-4.9": {
114
96
  "name": "Claude Opus 4.9",
115
97
  "limit": { "context": 1000000, "output": 64000 },
116
98
  "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
@@ -138,9 +120,11 @@ Resolution order:
138
120
  6. Hidden/manual model mapping
139
121
  7. Optimistic pass-through unless disabled
140
122
 
141
- This keeps new Kiro model ids usable before the package is updated. Use `extraModels` when a new model should appear in OpenCode's model picker immediately. Set `disableModelPassThrough: true` only when you need strict model governance.
123
+ This keeps new Kiro model ids visible before the package is updated. Use `extraModels` only when a new model should appear in OpenCode's model picker but is not yet listed by your installed Kiro CLI. Set `disableModelPassThrough: true` only when you need strict model governance.
124
+
125
+ The plugin injects `provider.kiro` automatically. You only need to add `provider.kiro.models` yourself when overriding OpenCode model-picker metadata, such as a display name or context limit. Use plugin `modelAliases` for aliases that should resolve one requested model id to another.
142
126
 
143
- `modelDiscoveryCommand` is optional and intentionally not guessed by default because Kiro CLI model-list flags may vary by version. When configured, stdout can be a JSON array, `{ "models": [...] }`, `{ "data": [...] }`, or one model id per line.
127
+ `modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to skip runtime discovery. Discovery stdout can be a JSON array, `{ "models": [...] }`, `{ "data": [...] }`, Kiro CLI list-models JSON, Kiro CLI plain list output, or one model id per line.
144
128
 
145
129
  ## Troubleshooting
146
130
 
@@ -12,6 +12,8 @@ export function cliChatArgs(request, options = {}) {
12
12
  return [
13
13
  "chat",
14
14
  "--no-interactive",
15
+ "--model",
16
+ request.modelId,
15
17
  ...(options.trustAllTools ? ["--trust-all-tools"] : []),
16
18
  promptForCli(request),
17
19
  ];
@@ -44,8 +46,12 @@ export class KiroCliChatTransport {
44
46
  const authError = result.stderr.toLowerCase().includes("not logged in");
45
47
  throw new KiroPluginError(result.stderr.trim() || "kiro-cli chat failed", authError ? "KIRO_AUTH_ERROR" : "KIRO_CLI_FAILED", authError ? 401 : 502);
46
48
  }
49
+ const text = sanitizeCliChatOutput(result.stdout);
50
+ if (!text) {
51
+ throw new KiroPluginError(result.stderr.trim() || "kiro-cli chat completed without returning assistant text", "KIRO_EMPTY_RESPONSE", 502);
52
+ }
47
53
  return {
48
- text: sanitizeCliChatOutput(result.stdout),
54
+ text,
49
55
  modelId: request.modelId,
50
56
  };
51
57
  }
package/dist/config.d.ts CHANGED
@@ -22,6 +22,7 @@ export interface KiroPluginOptions {
22
22
  }
23
23
  export declare const DEFAULT_PROVIDER_ID = "kiro";
24
24
  export declare const DEFAULT_REGION = "us-east-1";
25
+ export declare const DEFAULT_MODEL_DISCOVERY_COMMAND: readonly ["kiro-cli", "chat", "--list-models", "--format", "json"];
25
26
  export declare const DEFAULT_MODEL_CACHE_TTL_SECONDS: number;
26
27
  export declare const DEFAULT_MAX_ATTEMPTS = 3;
27
28
  export declare function loadOptions(raw?: unknown): KiroPluginOptions;
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export const DEFAULT_PROVIDER_ID = "kiro";
2
2
  export const DEFAULT_REGION = "us-east-1";
3
+ export const DEFAULT_MODEL_DISCOVERY_COMMAND = ["kiro-cli", "chat", "--list-models", "--format", "json"];
3
4
  export const DEFAULT_MODEL_CACHE_TTL_SECONDS = 6 * 60 * 60;
4
5
  export const DEFAULT_MAX_ATTEMPTS = 3;
5
6
  const BACKENDS = new Set(["auto", "fetch", "cli-chat", "acp"]);
@@ -53,13 +54,14 @@ export function loadOptions(raw = {}) {
53
54
  const profileArn = optionalString(input.profileArn);
54
55
  const userAgent = optionalString(input.userAgent);
55
56
  const agentMode = optionalString(input.agentMode);
57
+ const modelDiscoveryCommand = "modelDiscoveryCommand" in input ? stringArray(input.modelDiscoveryCommand) : [...DEFAULT_MODEL_DISCOVERY_COMMAND];
56
58
  return {
57
59
  providerID: typeof input.providerID === "string" && input.providerID ? input.providerID : DEFAULT_PROVIDER_ID,
58
60
  region: typeof input.region === "string" && input.region ? input.region : DEFAULT_REGION,
59
61
  ...(endpoint ? { endpoint } : {}),
60
62
  backend,
61
63
  modelDiscovery,
62
- modelDiscoveryCommand: stringArray(input.modelDiscoveryCommand),
64
+ modelDiscoveryCommand,
63
65
  modelCacheTtlSeconds: positiveNumber(input.modelCacheTtlSeconds, DEFAULT_MODEL_CACHE_TTL_SECONDS),
64
66
  ...(requestTimeoutMs ? { requestTimeoutMs } : {}),
65
67
  maxAttempts: positiveInteger(input.maxAttempts, DEFAULT_MAX_ATTEMPTS),
@@ -1,4 +1,4 @@
1
- import { errorResponse, UnsupportedBackendError } from "./errors.js";
1
+ import { errorResponse, KiroPluginError, UnsupportedBackendError } from "./errors.js";
2
2
  import { readOpenAIRequest, toKiroGenerateRequest } from "./request-adapter.js";
3
3
  import { toOpenAIChatResponse, toOpenAIChatStreamResponse } from "./response-adapter.js";
4
4
  const unsupportedTransport = {
@@ -6,12 +6,17 @@ const unsupportedTransport = {
6
6
  throw new UnsupportedBackendError();
7
7
  },
8
8
  };
9
- async function* streamGeneratedResponse(transport, request) {
10
- const response = await transport.generate(request);
9
+ function assertNonEmptyResponse(response) {
10
+ if (response.text || response.reasoning || (response.toolCalls?.length ?? 0) > 0)
11
+ return;
12
+ throw new KiroPluginError("Kiro backend returned an empty response.", "KIRO_EMPTY_RESPONSE", 502);
13
+ }
14
+ async function* responseToStream(response, fallbackModelId) {
15
+ assertNonEmptyResponse(response);
11
16
  if (response.reasoning)
12
- yield { type: "reasoning", text: response.reasoning, modelId: response.modelId ?? request.modelId };
17
+ yield { type: "reasoning", text: response.reasoning, modelId: response.modelId ?? fallbackModelId };
13
18
  if (response.text)
14
- yield { type: "text", text: response.text, modelId: response.modelId ?? request.modelId };
19
+ yield { type: "text", text: response.text, modelId: response.modelId ?? fallbackModelId };
15
20
  for (const toolCall of response.toolCalls ?? [])
16
21
  yield toolCall;
17
22
  }
@@ -25,9 +30,12 @@ export function createKiroFetch(options) {
25
30
  return toOpenAIChatStreamResponse(transport.stream(kiroRequest), kiroRequest.modelId);
26
31
  }
27
32
  if (request.stream === true) {
28
- return toOpenAIChatStreamResponse(streamGeneratedResponse(transport, kiroRequest), kiroRequest.modelId);
33
+ const response = await transport.generate(kiroRequest);
34
+ assertNonEmptyResponse(response);
35
+ return toOpenAIChatStreamResponse(responseToStream(response, kiroRequest.modelId), kiroRequest.modelId);
29
36
  }
30
37
  const response = await transport.generate(kiroRequest);
38
+ assertNonEmptyResponse(response);
31
39
  return toOpenAIChatResponse(response, kiroRequest.modelId);
32
40
  }
33
41
  catch (error) {
@@ -6,6 +6,9 @@ function record(value) {
6
6
  function stringValue(value) {
7
7
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
8
8
  }
9
+ function positiveNumberValue(value) {
10
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
11
+ }
9
12
  function fromItem(item) {
10
13
  if (typeof item === "string") {
11
14
  const id = normalizeModelName(item);
@@ -14,16 +17,25 @@ function fromItem(item) {
14
17
  const object = record(item);
15
18
  if (!object)
16
19
  return undefined;
17
- const id = stringValue(object.id) ?? stringValue(object.modelId) ?? stringValue(object.model) ?? stringValue(object.name);
20
+ const id = stringValue(object.id) ??
21
+ stringValue(object.modelId) ??
22
+ stringValue(object.model_id) ??
23
+ stringValue(object.model) ??
24
+ stringValue(object.model_name) ??
25
+ stringValue(object.name);
18
26
  if (!id)
19
27
  return undefined;
20
28
  const normalized = normalizeModelName(id);
21
29
  if (!normalized)
22
30
  return undefined;
23
31
  const name = stringValue(object.name) ?? stringValue(object.displayName) ?? stringValue(object.label);
32
+ const contextLimit = positiveNumberValue(object.contextLimit) ?? positiveNumberValue(object.context_window_tokens);
33
+ const outputLimit = positiveNumberValue(object.outputLimit) ?? positiveNumberValue(object.output_limit_tokens);
24
34
  return {
25
35
  id: normalized,
26
36
  ...(name ? { name } : {}),
37
+ ...(contextLimit ? { contextLimit } : {}),
38
+ ...(outputLimit ? { outputLimit } : {}),
27
39
  raw: item,
28
40
  };
29
41
  }
@@ -34,13 +46,18 @@ function fromJson(value) {
34
46
  if (!object)
35
47
  return [];
36
48
  const items = object.models ?? object.data ?? object.items;
37
- return Array.isArray(items) ? fromJson(items) : [];
49
+ if (Array.isArray(items))
50
+ return fromJson(items);
51
+ const configuredModel = object["chat.defaultModel"] ?? object.defaultModel ?? object.model;
52
+ return configuredModel ? fromJson([configuredModel]) : [];
38
53
  }
39
54
  function fromLines(raw) {
40
55
  return raw
41
56
  .split(/\r?\n/)
42
57
  .map((line) => line.trim())
43
- .filter((line) => line && !line.includes(" ") && !line.includes("\t"))
58
+ .map((line) => line.replace(/^\*\s*/, ""))
59
+ .map((line) => /^\S+/.exec(line)?.[0] ?? "")
60
+ .filter((line) => line === "auto" || /[.-]/.test(line))
44
61
  .map(fromItem)
45
62
  .filter((item) => item !== undefined);
46
63
  }
package/dist/models.js CHANGED
@@ -7,31 +7,61 @@ export const FALLBACK_MODELS = {
7
7
  modalities: TEXT_IMAGE_PDF,
8
8
  tool_call: true,
9
9
  },
10
+ "claude-fable-5": {
11
+ name: "Claude Fable 5",
12
+ limit: { context: 1_000_000, output: 64_000 },
13
+ modalities: TEXT_IMAGE_PDF,
14
+ tool_call: true,
15
+ },
16
+ "claude-sonnet-5": {
17
+ name: "Claude Sonnet 5",
18
+ limit: { context: 1_000_000, output: 64_000 },
19
+ modalities: TEXT_IMAGE_PDF,
20
+ tool_call: true,
21
+ },
10
22
  "claude-sonnet-4": {
11
- name: "Claude Sonnet 4",
23
+ name: "Claude Sonnet 4.0",
12
24
  limit: { context: 200_000, output: 64_000 },
13
25
  modalities: TEXT_IMAGE_PDF,
14
26
  tool_call: true,
15
27
  },
16
- "claude-sonnet-4-5": {
28
+ "claude-sonnet-4.5": {
17
29
  name: "Claude Sonnet 4.5",
18
30
  limit: { context: 200_000, output: 64_000 },
19
31
  modalities: TEXT_IMAGE_PDF,
20
32
  tool_call: true,
21
33
  },
22
- "claude-sonnet-4-6": {
34
+ "claude-sonnet-4.6": {
23
35
  name: "Claude Sonnet 4.6",
24
36
  limit: { context: 1_000_000, output: 64_000 },
25
37
  modalities: TEXT_IMAGE_PDF,
26
38
  tool_call: true,
27
39
  },
28
- "claude-opus-4-8": {
40
+ "claude-opus-4.8": {
29
41
  name: "Claude Opus 4.8",
42
+ limit: { context: 1_000_000, output: 128_000 },
43
+ modalities: TEXT_IMAGE_PDF,
44
+ tool_call: true,
45
+ },
46
+ "claude-opus-4.7": {
47
+ name: "Claude Opus 4.7",
48
+ limit: { context: 1_000_000, output: 64_000 },
49
+ modalities: TEXT_IMAGE_PDF,
50
+ tool_call: true,
51
+ },
52
+ "claude-opus-4.6": {
53
+ name: "Claude Opus 4.6",
30
54
  limit: { context: 1_000_000, output: 64_000 },
31
55
  modalities: TEXT_IMAGE_PDF,
32
56
  tool_call: true,
33
57
  },
34
- "claude-haiku-4-5": {
58
+ "claude-opus-4.5": {
59
+ name: "Claude Opus 4.5",
60
+ limit: { context: 200_000, output: 64_000 },
61
+ modalities: TEXT_IMAGE_PDF,
62
+ tool_call: true,
63
+ },
64
+ "claude-haiku-4.5": {
35
65
  name: "Claude Haiku 4.5",
36
66
  limit: { context: 200_000, output: 64_000 },
37
67
  modalities: { input: ["text", "image"], output: ["text"] },
package/dist/plugin.js CHANGED
@@ -21,6 +21,14 @@ function discoveredProviderModels(cache) {
21
21
  model.id,
22
22
  {
23
23
  name: model.name ?? model.id,
24
+ ...(model.contextLimit || model.outputLimit
25
+ ? {
26
+ limit: {
27
+ context: model.contextLimit ?? 200_000,
28
+ output: model.outputLimit ?? 64_000,
29
+ },
30
+ }
31
+ : {}),
24
32
  },
25
33
  ]));
26
34
  }
@@ -1,3 +1,4 @@
1
+ import { KiroPluginError } from "./errors.js";
1
2
  export function toOpenAIChatResponse(response, model) {
2
3
  const toolCalls = response.toolCalls ?? [];
3
4
  return Response.json({
@@ -47,6 +48,7 @@ export function toOpenAIChatStreamResponse(chunks, model) {
47
48
  const toolCallIndexes = new Map();
48
49
  let nextToolCallIndex = 0;
49
50
  let sawToolCall = false;
51
+ let sawDelta = false;
50
52
  for await (const chunk of chunks) {
51
53
  let delta;
52
54
  if (chunk.type === "tool_call") {
@@ -81,6 +83,7 @@ export function toOpenAIChatStreamResponse(chunks, model) {
81
83
  continue;
82
84
  delta = { content: chunk.text };
83
85
  }
86
+ sawDelta = true;
84
87
  controller.enqueue(encoder.encode(sse({
85
88
  id: `kiro-${crypto.randomUUID()}`,
86
89
  object: "chat.completion.chunk",
@@ -95,6 +98,9 @@ export function toOpenAIChatStreamResponse(chunks, model) {
95
98
  ],
96
99
  })));
97
100
  }
101
+ if (!sawDelta) {
102
+ throw new KiroPluginError("Kiro backend returned an empty response stream.", "KIRO_EMPTY_RESPONSE", 502);
103
+ }
98
104
  controller.enqueue(encoder.encode(sse({ choices: [{ index: 0, delta: {}, finish_reason: sawToolCall ? "tool_calls" : "stop" }] })));
99
105
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
100
106
  controller.close();
@@ -152,7 +152,7 @@ Before publishing, verify that new model ids can be used without a code release:
152
152
  "modelAliases": {
153
153
  "new-model": "new-model-id"
154
154
  },
155
- "modelDiscoveryCommand": ["kiro-cli", "models", "--json"]
155
+ "modelDiscoveryCommand": ["kiro-cli", "chat", "--list-models", "--format", "json"]
156
156
  }
157
157
  ]
158
158
  ]
@@ -161,7 +161,7 @@ Before publishing, verify that new model ids can be used without a code release:
161
161
 
162
162
  Checks:
163
163
 
164
- - If your installed `kiro-cli` has a model-list command, set `modelDiscoveryCommand` to that command and confirm discovered models appear in provider metadata.
164
+ - Confirm `kiro-cli chat --list-models --format json` succeeds and discovered models appear in provider metadata.
165
165
  - The model appears in OpenCode provider metadata.
166
166
  - The alias resolves to the configured id.
167
167
  - Unknown models still pass through when `disableModelPassThrough` is false.
@@ -13,15 +13,15 @@
13
13
  // "agentMode": "vibe",
14
14
  "modelCacheTtlSeconds": 21600,
15
15
  "modelDiscovery": "auto",
16
- "modelDiscoveryCommand": [],
16
+ "modelDiscoveryCommand": ["kiro-cli", "chat", "--list-models", "--format", "json"],
17
17
  "maxAttempts": 3,
18
18
  "requestTimeoutMs": 120000,
19
19
  "modelAliases": {
20
- "sonnet": "claude-sonnet-4.6",
21
- "opus": "claude-opus-4.6"
20
+ "sonnet": "claude-sonnet-5",
21
+ "opus": "claude-opus-4.8"
22
22
  },
23
23
  "extraModels": {
24
- "claude-opus-4-9": {
24
+ "claude-opus-4.9": {
25
25
  "name": "Claude Opus 4.9",
26
26
  "limit": { "context": 1000000, "output": 64000 },
27
27
  "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] },
@@ -34,14 +34,5 @@
34
34
  "trustAllTools": false
35
35
  }
36
36
  ]
37
- ],
38
- "provider": {
39
- "kiro": {
40
- "models": {
41
- "sonnet": {
42
- "name": "Claude Sonnet via Kiro"
43
- }
44
- }
45
- }
46
- }
37
+ ]
47
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",