@bogyie/opencode-kiro-plugin 0.2.3 → 0.2.5

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
@@ -39,7 +39,7 @@ The plugin resolves credentials in this order:
39
39
 
40
40
  Direct fetch mode requires an API key/token usable by the Kiro/CodeWhisperer client. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state.
41
41
 
42
- Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and fallback model preset count. Secrets are redacted in diagnostics.
42
+ Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Secrets are redacted in diagnostics.
43
43
 
44
44
  ## Backend Modes
45
45
 
@@ -74,7 +74,7 @@ Supported values:
74
74
 
75
75
  ## Model Churn Handling
76
76
 
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.
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`. Runtime discovery is the source of truth for the model picker; bundled metadata is not injected as available models by default.
78
78
 
79
79
  Useful options:
80
80
 
@@ -116,11 +116,11 @@ Resolution order:
116
116
  2. Name normalization, for example `claude-sonnet-4-6` to `claude-sonnet-4.6`
117
117
  3. Disabled model check
118
118
  4. Dynamic cache hit
119
- 5. Extra model preset cache hit
119
+ 5. Explicit `extraModels` cache hit
120
120
  6. Hidden/manual model mapping
121
121
  7. Optimistic pass-through unless disabled
122
122
 
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.
123
+ This keeps new Kiro model ids usable before the package is updated without advertising unavailable models in OpenCode's picker. Use `extraModels` only when you explicitly want a model to appear even though it is not listed by your installed Kiro CLI. Set `disableModelPassThrough: true` only when you need strict model governance.
124
124
 
125
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.
126
126
 
@@ -150,6 +150,12 @@ npm run typecheck
150
150
  npm run build
151
151
  ```
152
152
 
153
+ For a real Kiro smoke test that uses your local `kiro-cli` login, runs runtime model discovery, verifies the plugin does not advertise models absent from the real CLI list, and checks both non-streaming and streaming assistant responses:
154
+
155
+ ```sh
156
+ npm run smoke:kiro
157
+ ```
158
+
153
159
  For real Kiro/OpenCode validation, use [docs/e2e-validation.md](docs/e2e-validation.md).
154
160
 
155
161
  ## Release
@@ -0,0 +1,6 @@
1
+ import type { FetchAdapter } from "./fetch-adapter.js";
2
+ export interface LocalKiroServer {
3
+ readonly baseURL: string;
4
+ close(): Promise<void>;
5
+ }
6
+ export declare function startLocalKiroServer(fetchAdapter: FetchAdapter): Promise<LocalKiroServer>;
@@ -0,0 +1,78 @@
1
+ import { createServer } from "node:http";
2
+ function requestBody(req) {
3
+ return new Promise((resolve, reject) => {
4
+ const chunks = [];
5
+ req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
6
+ req.on("end", () => resolve(Buffer.concat(chunks)));
7
+ req.on("error", reject);
8
+ });
9
+ }
10
+ async function writeResponse(res, response) {
11
+ res.statusCode = response.status;
12
+ response.headers.forEach((value, key) => {
13
+ res.setHeader(key, value);
14
+ });
15
+ if (!response.body) {
16
+ res.end();
17
+ return;
18
+ }
19
+ const reader = response.body.getReader();
20
+ try {
21
+ while (true) {
22
+ const item = await reader.read();
23
+ if (item.done)
24
+ break;
25
+ res.write(Buffer.from(item.value));
26
+ }
27
+ res.end();
28
+ }
29
+ finally {
30
+ reader.releaseLock();
31
+ }
32
+ }
33
+ export async function startLocalKiroServer(fetchAdapter) {
34
+ const server = createServer(async (req, res) => {
35
+ try {
36
+ const body = await requestBody(req);
37
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
38
+ const init = {
39
+ method: req.method ?? "POST",
40
+ headers: req.headers,
41
+ };
42
+ if (body.length > 0)
43
+ init.body = new Uint8Array(body);
44
+ const response = await fetchAdapter(url, init);
45
+ await writeResponse(res, response);
46
+ }
47
+ catch (error) {
48
+ res.statusCode = 500;
49
+ res.setHeader("content-type", "application/json");
50
+ res.end(JSON.stringify({
51
+ error: {
52
+ message: error instanceof Error ? error.message : "Unknown local Kiro server error",
53
+ type: "KIRO_LOCAL_SERVER_ERROR",
54
+ code: "KIRO_LOCAL_SERVER_ERROR",
55
+ },
56
+ }));
57
+ }
58
+ });
59
+ await new Promise((resolve, reject) => {
60
+ server.once("error", reject);
61
+ server.listen(0, "127.0.0.1", () => {
62
+ server.off("error", reject);
63
+ resolve();
64
+ });
65
+ });
66
+ const address = server.address();
67
+ return {
68
+ baseURL: `http://127.0.0.1:${address.port}/v1`,
69
+ close: () => new Promise((resolve, reject) => {
70
+ server.close((error) => {
71
+ if (error)
72
+ reject(error);
73
+ else
74
+ resolve();
75
+ });
76
+ }),
77
+ };
78
+ }
package/dist/models.js CHANGED
@@ -1,100 +1,2 @@
1
- const TEXT_IMAGE_PDF = { input: ["text", "image", "pdf"], output: ["text"] };
2
- const TEXT_ONLY = { input: ["text"], output: ["text"] };
3
- export const FALLBACK_MODELS = {
4
- auto: {
5
- name: "Kiro Auto",
6
- limit: { context: 200_000, output: 64_000 },
7
- modalities: TEXT_IMAGE_PDF,
8
- tool_call: true,
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
- },
22
- "claude-sonnet-4": {
23
- name: "Claude Sonnet 4.0",
24
- limit: { context: 200_000, output: 64_000 },
25
- modalities: TEXT_IMAGE_PDF,
26
- tool_call: true,
27
- },
28
- "claude-sonnet-4.5": {
29
- name: "Claude Sonnet 4.5",
30
- limit: { context: 200_000, output: 64_000 },
31
- modalities: TEXT_IMAGE_PDF,
32
- tool_call: true,
33
- },
34
- "claude-sonnet-4.6": {
35
- name: "Claude Sonnet 4.6",
36
- limit: { context: 1_000_000, output: 64_000 },
37
- modalities: TEXT_IMAGE_PDF,
38
- tool_call: true,
39
- },
40
- "claude-opus-4.8": {
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",
54
- limit: { context: 1_000_000, output: 64_000 },
55
- modalities: TEXT_IMAGE_PDF,
56
- tool_call: true,
57
- },
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": {
65
- name: "Claude Haiku 4.5",
66
- limit: { context: 200_000, output: 64_000 },
67
- modalities: { input: ["text", "image"], output: ["text"] },
68
- tool_call: true,
69
- },
70
- "deepseek-3.2": {
71
- name: "DeepSeek 3.2",
72
- limit: { context: 128_000, output: 64_000 },
73
- modalities: TEXT_ONLY,
74
- tool_call: true,
75
- },
76
- "glm-5": {
77
- name: "GLM-5",
78
- limit: { context: 200_000, output: 64_000 },
79
- modalities: TEXT_ONLY,
80
- tool_call: true,
81
- },
82
- "minimax-m2.5": {
83
- name: "MiniMax M2.5",
84
- limit: { context: 200_000, output: 64_000 },
85
- modalities: TEXT_ONLY,
86
- tool_call: true,
87
- },
88
- "minimax-m2.1": {
89
- name: "MiniMax M2.1",
90
- limit: { context: 200_000, output: 64_000 },
91
- modalities: TEXT_ONLY,
92
- tool_call: true,
93
- },
94
- "qwen3-coder-next": {
95
- name: "Qwen3 Coder Next",
96
- limit: { context: 256_000, output: 64_000 },
97
- modalities: TEXT_ONLY,
98
- tool_call: true,
99
- },
100
- };
1
+ // Retained as an empty compatibility export. Runtime discovery is the source of truth.
2
+ export const FALLBACK_MODELS = {};
package/dist/plugin.js CHANGED
@@ -4,18 +4,11 @@ import { detectAuth, resolveApiKey } from "./auth.js";
4
4
  import { KiroCliChatTransport } from "./cli-transport.js";
5
5
  import { loadOptions } from "./config.js";
6
6
  import { createKiroFetch } from "./fetch-adapter.js";
7
+ import { startLocalKiroServer } from "./local-server.js";
7
8
  import { ModelCache } from "./model-cache.js";
8
9
  import { refreshModelCacheFromCommand } from "./model-discovery.js";
9
10
  import { ModelResolver, normalizeModelName } from "./model-resolver.js";
10
11
  import { CodeWhispererKiroTransport } from "./kiro-transport.js";
11
- import { FALLBACK_MODELS } from "./models.js";
12
- function mergeModels(extraModels, existing) {
13
- return {
14
- ...FALLBACK_MODELS,
15
- ...extraModels,
16
- ...(existing ?? {}),
17
- };
18
- }
19
12
  function discoveredProviderModels(cache) {
20
13
  return Object.fromEntries(cache.all().map((model) => [
21
14
  model.id,
@@ -32,6 +25,37 @@ function discoveredProviderModels(cache) {
32
25
  },
33
26
  ]));
34
27
  }
28
+ function modelRecord(value) {
29
+ if (!value || typeof value !== "object" || Array.isArray(value))
30
+ return {};
31
+ return Object.fromEntries(Object.entries(value).filter((entry) => Boolean(entry[0]) && Boolean(entry[1]) && typeof entry[1] === "object" && !Array.isArray(entry[1])));
32
+ }
33
+ function bearerToken(init) {
34
+ const header = new Headers(init?.headers).get("authorization");
35
+ const match = header?.match(/^Bearer\s+(.+)$/i);
36
+ return match?.[1] || undefined;
37
+ }
38
+ function localTransport(options, accessToken) {
39
+ if (options.backend === "acp")
40
+ return new KiroAcpTransport(acpTransportOptions(options));
41
+ if (options.backend === "cli-chat") {
42
+ return new KiroCliChatTransport({
43
+ trustAllTools: options.trustAllTools,
44
+ ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
45
+ });
46
+ }
47
+ const apiKey = accessToken || process.env.KIRO_API_KEY;
48
+ if (apiKey && apiKey !== "kiro-plugin-local-transport") {
49
+ return new CodeWhispererKiroTransport(fetchTransportOptions(options, apiKey));
50
+ }
51
+ if (options.backend === "auto") {
52
+ return new KiroCliChatTransport({
53
+ trustAllTools: options.trustAllTools,
54
+ ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
55
+ });
56
+ }
57
+ return undefined;
58
+ }
35
59
  export function acpTransportOptions(options) {
36
60
  return {
37
61
  trustAllTools: options.trustAllTools,
@@ -53,9 +77,13 @@ export function fetchTransportOptions(options, accessToken) {
53
77
  export function createKiroPlugin() {
54
78
  return async (_input, rawOptions) => {
55
79
  const options = loadOptions(rawOptions);
56
- const baseURL = options.endpoint ?? `https://q.${options.region}.amazonaws.com`;
57
80
  const modelCache = new ModelCache(options.modelCacheTtlSeconds);
58
- modelCache.update([...Object.keys(FALLBACK_MODELS), ...Object.keys(options.extraModels)].map((id) => ({ id: normalizeModelName(id) })));
81
+ let localServer;
82
+ let configuredModels = { ...options.extraModels };
83
+ const disabledModels = new Set(options.disabledModels.map(normalizeModelName));
84
+ if (Object.keys(options.extraModels).length > 0) {
85
+ modelCache.update(Object.keys(options.extraModels).map((id) => ({ id: normalizeModelName(id) })));
86
+ }
59
87
  let discovery;
60
88
  let discoveryAttempted = false;
61
89
  const discoverIfStale = async () => {
@@ -79,16 +107,39 @@ export function createKiroPlugin() {
79
107
  disabledModels: options.disabledModels,
80
108
  disablePassThrough: options.disableModelPassThrough,
81
109
  });
110
+ const ensureLocalServer = async () => {
111
+ if (localServer)
112
+ return localServer;
113
+ const localFetch = async (input, init) => {
114
+ const transport = localTransport(options, bearerToken(init));
115
+ return createKiroFetch({
116
+ resolver,
117
+ ...(transport ? { transport } : {}),
118
+ })(input, init);
119
+ };
120
+ localServer = await startLocalKiroServer(localFetch);
121
+ return localServer;
122
+ };
82
123
  return {
124
+ dispose: async () => {
125
+ await localServer?.close();
126
+ localServer = undefined;
127
+ },
83
128
  config: async (config) => {
129
+ await discoverIfStale();
84
130
  config.provider ??= {};
85
131
  config.provider[options.providerID] ??= {};
86
132
  const provider = config.provider[options.providerID];
133
+ const server = await ensureLocalServer();
134
+ configuredModels = {
135
+ ...options.extraModels,
136
+ ...modelRecord(provider.models),
137
+ };
87
138
  provider.name ??= "Kiro";
88
139
  provider.npm = "@ai-sdk/openai-compatible";
89
- provider.api ??= baseURL;
140
+ provider.api = server.baseURL;
90
141
  provider.options ??= {};
91
- provider.models = mergeModels(options.extraModels, provider.models);
142
+ provider.models = configuredModels;
92
143
  },
93
144
  auth: {
94
145
  provider: options.providerID,
@@ -115,36 +166,19 @@ export function createKiroPlugin() {
115
166
  loader: async (auth) => {
116
167
  await discoverIfStale();
117
168
  const apiKey = await resolveApiKey(auth);
118
- const transport = options.backend === "acp"
119
- ? new KiroAcpTransport(acpTransportOptions(options))
120
- : options.backend === "cli-chat"
121
- ? new KiroCliChatTransport({
122
- trustAllTools: options.trustAllTools,
123
- ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
124
- })
125
- : apiKey
126
- ? new CodeWhispererKiroTransport(fetchTransportOptions(options, apiKey))
127
- : options.backend === "auto"
128
- ? new KiroCliChatTransport({
129
- trustAllTools: options.trustAllTools,
130
- ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
131
- })
132
- : undefined;
169
+ const server = await ensureLocalServer();
133
170
  return {
134
- apiKey,
135
- baseURL,
136
- fetch: createKiroFetch({
137
- resolver,
138
- ...(transport ? { transport } : {}),
139
- }),
171
+ apiKey: apiKey || "kiro-plugin-local-transport",
172
+ baseURL: server.baseURL,
140
173
  };
141
174
  },
142
175
  },
143
176
  tool: {
144
177
  kiro_status: tool({
145
- description: "Show Kiro plugin backend, auth, region, and model fallback status.",
178
+ description: "Show Kiro plugin backend, auth, region, and discovered model status.",
146
179
  args: {},
147
180
  execute: async () => {
181
+ await discoverIfStale();
148
182
  const auth = await detectAuth();
149
183
  return {
150
184
  title: "Kiro status",
@@ -154,7 +188,7 @@ export function createKiroPlugin() {
154
188
  `region: ${auth.region}`,
155
189
  `auth: ${auth.method}`,
156
190
  `authenticated: ${auth.authenticated ? "yes" : "no"}`,
157
- `models: ${Object.keys(FALLBACK_MODELS).length} fallback presets`,
191
+ `models: ${modelCache.ids().length} discovered/cache entries`,
158
192
  auth.message,
159
193
  ].join("\n"),
160
194
  metadata: {
@@ -172,22 +206,35 @@ export function createKiroPlugin() {
172
206
  id: options.providerID,
173
207
  models: async (provider) => {
174
208
  await discoverIfStale();
175
- const models = {
176
- ...discoveredProviderModels(modelCache),
177
- ...(provider.models ?? {}),
178
- };
179
- return Object.fromEntries(Object.entries(models).map(([id, model]) => [
180
- id,
181
- {
182
- ...model,
183
- api: {
184
- ...(model.api ?? {}),
185
- id,
186
- npm: "@ai-sdk/openai-compatible",
187
- url: baseURL,
209
+ const discovered = discoveredProviderModels(modelCache);
210
+ const existing = modelRecord(provider.models);
211
+ const allowedModelIDs = new Set([
212
+ ...Object.keys(discovered),
213
+ ...Object.keys(configuredModels),
214
+ ...Object.keys(options.hiddenModels),
215
+ ]);
216
+ const server = await ensureLocalServer();
217
+ return Object.fromEntries([...allowedModelIDs]
218
+ .filter((id) => !disabledModels.has(normalizeModelName(id)))
219
+ .map((id) => {
220
+ const model = {
221
+ ...(discovered[id] ?? {}),
222
+ ...(configuredModels[id] ?? {}),
223
+ ...(existing[id] ?? {}),
224
+ };
225
+ return [
226
+ id,
227
+ {
228
+ ...model,
229
+ api: {
230
+ ...(model.api ?? {}),
231
+ id,
232
+ npm: "@ai-sdk/openai-compatible",
233
+ url: server.baseURL,
234
+ },
188
235
  },
189
- },
190
- ]));
236
+ ];
237
+ }));
191
238
  },
192
239
  },
193
240
  };
@@ -1,4 +1,4 @@
1
- import { KiroPluginError } from "./errors.js";
1
+ import { KiroPluginError, normalizeError } from "./errors.js";
2
2
  export function toOpenAIChatResponse(response, model) {
3
3
  const toolCalls = response.toolCalls ?? [];
4
4
  return Response.json({
@@ -40,17 +40,37 @@ export function toOpenAIChatResponse(response, model) {
40
40
  function sse(data) {
41
41
  return `data: ${JSON.stringify(data)}\n\n`;
42
42
  }
43
+ function streamChunk(id, created, model, delta, finishReason) {
44
+ return {
45
+ id,
46
+ object: "chat.completion.chunk",
47
+ created,
48
+ model,
49
+ choices: [
50
+ {
51
+ index: 0,
52
+ delta,
53
+ finish_reason: finishReason,
54
+ },
55
+ ],
56
+ };
57
+ }
43
58
  export function toOpenAIChatStreamResponse(chunks, model) {
44
59
  const encoder = new TextEncoder();
45
60
  const body = new ReadableStream({
46
61
  async start(controller) {
62
+ const id = `kiro-${crypto.randomUUID()}`;
63
+ const created = Math.floor(Date.now() / 1000);
64
+ let responseModel = model;
47
65
  try {
48
66
  const toolCallIndexes = new Map();
49
67
  let nextToolCallIndex = 0;
50
68
  let sawToolCall = false;
51
69
  let sawDelta = false;
70
+ controller.enqueue(encoder.encode(sse(streamChunk(id, created, responseModel, { role: "assistant" }, null))));
52
71
  for await (const chunk of chunks) {
53
72
  let delta;
73
+ responseModel = chunk.modelId ?? responseModel;
54
74
  if (chunk.type === "tool_call") {
55
75
  sawToolCall = true;
56
76
  let index = toolCallIndexes.get(chunk.id);
@@ -84,29 +104,26 @@ export function toOpenAIChatStreamResponse(chunks, model) {
84
104
  delta = { content: chunk.text };
85
105
  }
86
106
  sawDelta = true;
87
- controller.enqueue(encoder.encode(sse({
88
- id: `kiro-${crypto.randomUUID()}`,
89
- object: "chat.completion.chunk",
90
- created: Math.floor(Date.now() / 1000),
91
- model: chunk.modelId ?? model,
92
- choices: [
93
- {
94
- index: 0,
95
- delta,
96
- finish_reason: null,
97
- },
98
- ],
99
- })));
107
+ controller.enqueue(encoder.encode(sse(streamChunk(id, created, responseModel, delta, null))));
100
108
  }
101
109
  if (!sawDelta) {
102
110
  throw new KiroPluginError("Kiro backend returned an empty response stream.", "KIRO_EMPTY_RESPONSE", 502);
103
111
  }
104
- controller.enqueue(encoder.encode(sse({ choices: [{ index: 0, delta: {}, finish_reason: sawToolCall ? "tool_calls" : "stop" }] })));
112
+ controller.enqueue(encoder.encode(sse(streamChunk(id, created, responseModel, {}, sawToolCall ? "tool_calls" : "stop"))));
105
113
  controller.enqueue(encoder.encode("data: [DONE]\n\n"));
106
114
  controller.close();
107
115
  }
108
116
  catch (error) {
109
- controller.error(error);
117
+ const known = normalizeError(error);
118
+ controller.enqueue(encoder.encode(sse({
119
+ error: {
120
+ message: known.message,
121
+ type: known.code,
122
+ code: known.code,
123
+ },
124
+ })));
125
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
126
+ controller.close();
110
127
  }
111
128
  },
112
129
  });
@@ -189,7 +189,7 @@ Record any real Kiro/API limitations in `CHANGELOG.md` before publishing.
189
189
  - `kiro-cli whoami` succeeded with an IAM Identity Center session; account identifiers were not recorded here.
190
190
  - `kiro-cli chat --no-interactive "Say hello in one short sentence."` returned assistant text.
191
191
  - Built `KiroCliChatTransport` returned assistant text with `requestTimeoutMs: 30000`.
192
- - `opencode models kiro` in a temporary project using `file:/Users/boggy/repo/github.com/bogyie/opencode-kiro-plugin` listed `kiro/auto` and fallback models.
192
+ - `opencode models kiro` in a temporary project using `file:/Users/boggy/repo/github.com/bogyie/opencode-kiro-plugin` should list models returned by runtime discovery, not bundled fallback presets.
193
193
  - `opencode run -m kiro/auto --format json "Say hello in one short sentence."` returned a text event and `finish_reason: stop` through the local plugin with `backend: "cli-chat"`.
194
194
  - `kiro-cli acp --verbose` returned a valid `initialize` JSON-RPC response but also emitted a non-JSON stdout log line; the stdio client now ignores those log lines.
195
195
  - Built `KiroAcpTransport` still failed at session creation with `KIRO_ACP_PROCESS_EXITED` because Kiro CLI 2.6.1 exits with code 0 after the current `session/new` request. Full ACP prompt E2E still needs the current Kiro ACP session schema confirmed before release.
@@ -46,7 +46,7 @@
46
46
  - `config` hook에서 `provider.kiro` 생성
47
47
  - `npm: "@ai-sdk/openai-compatible"` 설정
48
48
  - provider-level `api` 또는 `options.baseURL` 설정
49
- - fallback model metadata 주입
49
+ - runtime discovery model metadata 주입
50
50
  - 사용자가 이미 정의한 provider/model 설정은 덮어쓰지 않고 merge
51
51
  - `enabled_providers`/`disabled_providers`와 충돌하지 않게 처리
52
52
 
@@ -54,7 +54,7 @@
54
54
 
55
55
  - 빈 config에 Kiro provider가 생깁니다.
56
56
  - 사용자 override가 유지됩니다.
57
- - model limit, modality, variant metadata가 기본값으로 들어갑니다.
57
+ - discovery 또는 사용자 override에서 온 model limit, modality, variant metadata가 들어갑니다.
58
58
 
59
59
  ## Milestone 2: Model Resolver
60
60
 
@@ -72,9 +72,9 @@
72
72
  - TTL cache
73
73
  - stale detection
74
74
  - refresh coordination
75
- - fallback preset loading
75
+ - explicit extra model loading
76
76
  - `src/models.ts`
77
- - fallback preset only
77
+ - optional metadata helpers only
78
78
  - runtime validation에는 사용하지 않음
79
79
  - config knobs
80
80
  - `modelCacheTtlSeconds`
@@ -274,7 +274,7 @@ test/
274
274
 
275
275
  - Model churn
276
276
  - pass-through를 기본값으로 둡니다.
277
- - fallback preset cache bootstrap과 UI 표시용으로만 씁니다.
277
+ - model picker는 runtime discovery 결과를 기준으로 하고, static preset 사용 가능 모델처럼 표시하지 않습니다.
278
278
 
279
279
  - Credential safety
280
280
  - token 값을 로그에 남기지 않습니다.
@@ -129,7 +129,7 @@ OpenCode에 Kiro provider를 plugin 형태로 추가합니다. 단, Kiro의 공
129
129
  3. Dynamic cache
130
130
  - Kiro model list API 또는 CLI discovery 결과를 TTL cache에 저장합니다.
131
131
  - cache가 비어 있거나 stale이면 refresh를 시도합니다.
132
- - refresh 실패 시 fallback preset을 사용하되 경고를 남깁니다.
132
+ - refresh 실패 시 static preset을 사용 가능 모델처럼 표시하지 않습니다.
133
133
 
134
134
  4. Hidden/manual models
135
135
  - 공식 list에는 없지만 동작하는 모델을 user config로 추가할 수 있게 합니다.
@@ -160,8 +160,8 @@ OpenCode에 Kiro provider를 plugin 형태로 추가합니다. 단, Kiro의 공
160
160
 
161
161
  설계 원칙:
162
162
 
163
- - fallback preset은 마지막 안전망이지 truth source가 아닙니다.
164
- - `SUPPORTED_MODELS` 같은 상수는 UI 표시와 tests용으로만 사용하고 runtime validation에는 쓰지 않습니다.
163
+ - static preset은 truth source가 아니며, 기본 model picker에 주입하지 않습니다.
164
+ - `SUPPORTED_MODELS` 같은 상수는 tests나 opt-in metadata용으로만 사용하고 runtime validation에는 쓰지 않습니다.
165
165
  - 새 모델 출시는 config 업데이트 없이 pass-through로 먼저 사용할 수 있어야 합니다.
166
166
  - `/models` 또는 OpenCode provider model list는 cache + hidden + alias를 합친 view를 반환합니다.
167
167
 
@@ -174,7 +174,7 @@ src/
174
174
  kiro-cli.ts # kiro-cli discovery, version, whoami
175
175
  acp-client.ts # JSON-RPC over stdio client
176
176
  chat-adapter.ts # no-interactive fallback
177
- models.ts # fallback presets and metadata defaults
177
+ models.ts # optional metadata defaults
178
178
  model-resolver.ts # alias, normalization, dynamic cache, hidden models, pass-through
179
179
  model-cache.ts # TTL cache and refresh coordination
180
180
  errors.ts # error normalization
@@ -210,7 +210,7 @@ test/
210
210
 
211
211
  4. OpenCode integration
212
212
  - provide config injection or documented config snippet
213
- - add model resolver with fallback presets
213
+ - add model resolver with runtime discovery and explicit extra models
214
214
  - expose a custom tool or command for diagnostics
215
215
 
216
216
  5. Tests
@@ -114,7 +114,7 @@ Kiro 모델 목록은 tier, region, release timing에 따라 바뀔 수 있으
114
114
  - startup 시 model discovery를 시도합니다.
115
115
  - discovery 결과는 TTL cache에 저장합니다.
116
116
  - cache가 stale이면 background refresh를 시도합니다.
117
- - refresh 실패 시 fallback preset으로 시작합니다.
117
+ - refresh 실패 시 static preset 사용 가능 모델처럼 표시하지 않고, 사용자 `extraModels`와 pass-through만 허용합니다.
118
118
  - 사용자가 `hiddenModels`, `modelAliases`, `disabledModels`를 설정할 수 있게 합니다.
119
119
  - 알 수 없는 모델은 기본적으로 pass-through합니다.
120
120
  - Kiro가 거절한 경우 같은 model family의 후보를 제안합니다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bogyie/opencode-kiro-plugin",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "OpenCode plugin for using Kiro as a provider adapter",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,7 @@
24
24
  "build": "npm run clean && tsc -p tsconfig.build.json",
25
25
  "prepack": "npm run build",
26
26
  "smoke:package": "npm run build && node scripts/smoke-package.mjs",
27
+ "smoke:kiro": "npm run build && node scripts/real-kiro-smoke.mjs",
27
28
  "test": "bun test test/*.test.ts",
28
29
  "typecheck": "tsc --noEmit"
29
30
  },