@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 +10 -4
- package/dist/local-server.d.ts +6 -0
- package/dist/local-server.js +78 -0
- package/dist/models.js +2 -100
- package/dist/plugin.js +97 -50
- package/dist/response-adapter.js +33 -16
- package/docs/e2e-validation.md +1 -1
- package/docs/implementation-plan.md +5 -5
- package/docs/implementation-strategy.md +5 -5
- package/docs/kiro-integration-notes.md +1 -1
- package/package.json +2 -1
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
|
|
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`.
|
|
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.
|
|
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
|
|
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,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
|
-
|
|
2
|
-
const
|
|
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
|
-
|
|
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
|
|
140
|
+
provider.api = server.baseURL;
|
|
90
141
|
provider.options ??= {};
|
|
91
|
-
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
|
|
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
|
|
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: ${
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
};
|
package/dist/response-adapter.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
});
|
package/docs/e2e-validation.md
CHANGED
|
@@ -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`
|
|
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
|
-
-
|
|
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
|
-
-
|
|
75
|
+
- explicit extra model loading
|
|
76
76
|
- `src/models.ts`
|
|
77
|
-
-
|
|
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
|
-
-
|
|
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 실패 시
|
|
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
|
-
-
|
|
164
|
-
- `SUPPORTED_MODELS` 같은 상수는
|
|
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 #
|
|
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
|
|
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 실패 시
|
|
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
|
+
"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
|
},
|