@bogyie/opencode-kiro-plugin 0.3.12 → 0.3.15
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 +5 -5
- package/dist/fetch-adapter.d.ts +5 -0
- package/dist/fetch-adapter.js +28 -0
- package/dist/model-cache-store.d.ts +6 -0
- package/dist/model-cache-store.js +49 -0
- package/dist/plugin.js +34 -9
- package/docs/kiro-integration-notes.md +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ The plugin resolves credentials in this order:
|
|
|
38
38
|
3. Local Kiro CLI session token from the Kiro CLI SQLite store
|
|
39
39
|
4. `kiro-cli whoami` diagnostics for CLI session visibility
|
|
40
40
|
|
|
41
|
-
OpenCode startup does not start Kiro login
|
|
41
|
+
OpenCode startup does not start Kiro login. It does run best-effort model discovery with `kiro-cli chat --list-models --format json` so the model picker can track the current Kiro CLI list. If startup discovery succeeds, the result is stored in a local cache and used immediately. If startup discovery fails, the plugin keeps the last stored model list. If no stored list exists yet, it injects an `auto` placeholder model so Kiro still appears in OpenCode's provider connector.
|
|
42
42
|
|
|
43
43
|
You can open OpenCode's provider connector, choose Kiro, and select `Kiro device login`. The connector runs `kiro-cli login --use-device-flow`, waits for the local Kiro CLI session to become authenticated, then stores a local transport marker in OpenCode auth. If `login.identityProvider`, `login.region`, `login.license`, or `login.extraArgs` are configured, those options are passed to `kiro-cli login` along with `--use-device-flow`. After login succeeds, the plugin also tries to refresh the runtime model cache. If no OpenCode connector marker or API key is configured, direct fetch still reads the active Kiro CLI session token and calls Kiro's REST/EventStream endpoint directly. If an API/model call fails with an auth error, the selected transport starts the same configured Kiro CLI login flow and retries the request once. `cli-chat` mode uses the official `kiro-cli chat --no-interactive` surface and depends on the local Kiro CLI login state. `acp` mode uses the official `kiro-cli acp` surface, but is still treated as an explicit backend while its real-world protocol behavior is validated across Kiro CLI versions.
|
|
44
44
|
|
|
@@ -64,7 +64,7 @@ For AWS IAM Identity Center login, configure the default device-flow Start URL s
|
|
|
64
64
|
|
|
65
65
|
For IAM Identity Center, configure `login.identityProvider` and `login.region` in plugin options so they are passed to `kiro-cli login --use-device-flow`.
|
|
66
66
|
|
|
67
|
-
Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Use `kiro_refresh_models` when you explicitly want to run the configured model discovery command and update the in-memory model cache. Secrets are redacted in diagnostics.
|
|
67
|
+
Use the `kiro_status` plugin tool to inspect provider id, backend, region, auth method, and discovered model count. Use `kiro_refresh_models` when you explicitly want to run the configured model discovery command and update the in-memory and stored model cache. Secrets are redacted in diagnostics.
|
|
68
68
|
|
|
69
69
|
## Backend Modes
|
|
70
70
|
|
|
@@ -105,7 +105,7 @@ Supported values:
|
|
|
105
105
|
|
|
106
106
|
## Model Churn Handling
|
|
107
107
|
|
|
108
|
-
The resolver intentionally avoids a hard whitelist.
|
|
108
|
+
The resolver intentionally avoids a hard whitelist. At startup, the plugin reads the current Kiro CLI model list with `kiro-cli chat --list-models --format json` and stores successful results in a local cache. Failed refreshes do not clear the last successful cache. If there is no successful cache yet, startup falls back to the `auto` placeholder so the provider can still be connected before discovery succeeds.
|
|
109
109
|
|
|
110
110
|
Useful options:
|
|
111
111
|
|
|
@@ -151,11 +151,11 @@ Resolution order:
|
|
|
151
151
|
6. Hidden/manual model mapping
|
|
152
152
|
7. Optimistic pass-through unless disabled
|
|
153
153
|
|
|
154
|
-
This keeps new Kiro model ids usable before the package is updated without advertising unavailable models in OpenCode's picker after discovery has succeeded. 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.
|
|
154
|
+
This keeps new Kiro model ids usable before the package is updated without advertising unavailable models in OpenCode's picker after discovery has succeeded. The stored model cache lives at `$OPENCODE_KIRO_MODEL_CACHE` when set, otherwise `$XDG_CACHE_HOME/opencode-kiro-plugin/models.json` or `~/.cache/opencode-kiro-plugin/models.json`. 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.
|
|
155
155
|
|
|
156
156
|
The plugin injects `provider.kiro` with the `auto` placeholder needed for OpenCode's provider connector. Define `provider.kiro.models` yourself only when you need explicit 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.
|
|
157
157
|
|
|
158
|
-
`modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to disable `kiro_refresh_models
|
|
158
|
+
`modelDiscoveryCommand` defaults to `["kiro-cli", "chat", "--list-models", "--format", "json"]`. Set `modelDiscovery` to `"off"` to disable startup discovery and `kiro_refresh_models`; in that mode the plugin uses any stored cache and then the `auto` fallback. Discovery is best-effort and does not start login during OpenCode startup. `/v1/models` also uses only this discovery/cache path: it never invokes the chat transport or login fallback, and returns the cached list or `auto` if discovery fails. Authenticate manually through the connector or let a real chat completion request handle auth failure. A successful connector login also triggers a forced model refresh. 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.
|
|
159
159
|
|
|
160
160
|
## Troubleshooting
|
|
161
161
|
|
package/dist/fetch-adapter.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface KiroTransport {
|
|
|
8
8
|
export interface KiroFetchOptions {
|
|
9
9
|
readonly resolver: ModelResolver;
|
|
10
10
|
readonly transport?: KiroTransport;
|
|
11
|
+
readonly models?: () => Promise<ReadonlyArray<string | OpenAIModelListItem>>;
|
|
11
12
|
}
|
|
12
13
|
export type FetchAdapter = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
14
|
+
export interface OpenAIModelListItem {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly [key: string]: unknown;
|
|
17
|
+
}
|
|
13
18
|
export declare function createKiroFetch(options: KiroFetchOptions): FetchAdapter;
|
package/dist/fetch-adapter.js
CHANGED
|
@@ -20,10 +20,38 @@ async function* responseToStream(response, fallbackModelId) {
|
|
|
20
20
|
for (const toolCall of response.toolCalls ?? [])
|
|
21
21
|
yield toolCall;
|
|
22
22
|
}
|
|
23
|
+
function requestPath(input) {
|
|
24
|
+
const raw = input instanceof Request ? input.url : input.toString();
|
|
25
|
+
const pathname = new URL(raw, "http://127.0.0.1").pathname.replace(/\/+$/, "");
|
|
26
|
+
return pathname || "/";
|
|
27
|
+
}
|
|
28
|
+
function isModelsPath(pathname) {
|
|
29
|
+
return pathname === "/v1/models" || pathname === "/models";
|
|
30
|
+
}
|
|
31
|
+
function toOpenAIModel(item) {
|
|
32
|
+
const id = typeof item === "string" ? item : item.id;
|
|
33
|
+
const extra = typeof item === "string" ? { id } : item;
|
|
34
|
+
return {
|
|
35
|
+
...extra,
|
|
36
|
+
id,
|
|
37
|
+
object: "model",
|
|
38
|
+
created: typeof extra.created === "number" ? extra.created : 0,
|
|
39
|
+
owned_by: typeof extra.owned_by === "string" ? extra.owned_by : "kiro",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async function toOpenAIModelsResponse(models) {
|
|
43
|
+
const data = models ? (await models()).filter((item) => (typeof item === "string" ? item.trim() : item.id.trim())).map(toOpenAIModel) : [];
|
|
44
|
+
return Response.json({
|
|
45
|
+
object: "list",
|
|
46
|
+
data,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
23
49
|
export function createKiroFetch(options) {
|
|
24
50
|
const transport = options.transport ?? unsupportedTransport;
|
|
25
51
|
return async (input, init) => {
|
|
26
52
|
try {
|
|
53
|
+
if (isModelsPath(requestPath(input)))
|
|
54
|
+
return toOpenAIModelsResponse(options.models);
|
|
27
55
|
const request = await readOpenAIRequest(input, init);
|
|
28
56
|
const kiroRequest = toKiroGenerateRequest(request, options.resolver);
|
|
29
57
|
if (request.stream === true && transport.stream) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CachedModelInfo } from "./model-cache.js";
|
|
2
|
+
export declare function loadStoredModelCache(): Promise<{
|
|
3
|
+
models: CachedModelInfo[];
|
|
4
|
+
updatedAt?: number;
|
|
5
|
+
}>;
|
|
6
|
+
export declare function saveStoredModelCache(models: ReadonlyArray<CachedModelInfo>, updatedAt?: number): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
function cachePath() {
|
|
5
|
+
if (process.env.OPENCODE_KIRO_MODEL_CACHE)
|
|
6
|
+
return process.env.OPENCODE_KIRO_MODEL_CACHE;
|
|
7
|
+
const base = process.env.XDG_CACHE_HOME || join(homedir() || tmpdir(), ".cache");
|
|
8
|
+
return join(base, "opencode-kiro-plugin", "models.json");
|
|
9
|
+
}
|
|
10
|
+
function modelInfo(value) {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
|
+
return undefined;
|
|
13
|
+
const input = value;
|
|
14
|
+
if (typeof input.id !== "string" || !input.id.trim())
|
|
15
|
+
return undefined;
|
|
16
|
+
const name = typeof input.name === "string" && input.name.trim() ? input.name.trim() : undefined;
|
|
17
|
+
const contextLimit = typeof input.contextLimit === "number" && Number.isFinite(input.contextLimit) ? input.contextLimit : undefined;
|
|
18
|
+
const outputLimit = typeof input.outputLimit === "number" && Number.isFinite(input.outputLimit) ? input.outputLimit : undefined;
|
|
19
|
+
return {
|
|
20
|
+
id: input.id.trim(),
|
|
21
|
+
...(name ? { name } : {}),
|
|
22
|
+
...(contextLimit ? { contextLimit } : {}),
|
|
23
|
+
...(outputLimit ? { outputLimit } : {}),
|
|
24
|
+
...("raw" in input ? { raw: input.raw } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function loadStoredModelCache() {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(await readFile(cachePath(), "utf8"));
|
|
30
|
+
const models = Array.isArray(parsed.models) ? parsed.models.map(modelInfo).filter((item) => Boolean(item)) : [];
|
|
31
|
+
return {
|
|
32
|
+
models,
|
|
33
|
+
...(typeof parsed.updatedAt === "number" && Number.isFinite(parsed.updatedAt) ? { updatedAt: parsed.updatedAt } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { models: [] };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function saveStoredModelCache(models, updatedAt = Date.now()) {
|
|
41
|
+
try {
|
|
42
|
+
const path = cachePath();
|
|
43
|
+
await mkdir(dirname(path), { recursive: true });
|
|
44
|
+
await writeFile(path, JSON.stringify({ updatedAt, models }, null, 2), "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Best-effort cache only.
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -6,7 +6,8 @@ import { loadOptions } from "./config.js";
|
|
|
6
6
|
import { createKiroFetch } from "./fetch-adapter.js";
|
|
7
7
|
import { startLocalKiroServer } from "./local-server.js";
|
|
8
8
|
import { ModelCache } from "./model-cache.js";
|
|
9
|
-
import {
|
|
9
|
+
import { discoverModelsFromCommand } from "./model-discovery.js";
|
|
10
|
+
import { loadStoredModelCache, saveStoredModelCache } from "./model-cache-store.js";
|
|
10
11
|
import { ModelResolver, normalizeModelName } from "./model-resolver.js";
|
|
11
12
|
import { KiroRestTransport } from "./kiro-rest-transport.js";
|
|
12
13
|
const PLACEHOLDER_MODEL_ID = "auto";
|
|
@@ -48,6 +49,12 @@ function visibleProviderModels(cache, configuredModels, hiddenModels, disabledMo
|
|
|
48
49
|
return models;
|
|
49
50
|
return { [PLACEHOLDER_MODEL_ID]: PLACEHOLDER_MODEL };
|
|
50
51
|
}
|
|
52
|
+
function extraModelInfos(models) {
|
|
53
|
+
return Object.keys(models).map((id) => ({ id: normalizeModelName(id) }));
|
|
54
|
+
}
|
|
55
|
+
function mergeModelInfos(discovered, extraModels) {
|
|
56
|
+
return [...new Map([...discovered, ...extraModelInfos(extraModels)].map((model) => [model.id, model])).values()];
|
|
57
|
+
}
|
|
51
58
|
function bearerToken(init) {
|
|
52
59
|
const header = new Headers(init?.headers).get("authorization");
|
|
53
60
|
const match = header?.match(/^Bearer\s+(.+)$/i);
|
|
@@ -67,13 +74,19 @@ export function effectiveBackend(options, accessToken) {
|
|
|
67
74
|
}
|
|
68
75
|
function localTransport(options, accessToken) {
|
|
69
76
|
const backend = effectiveBackend(options, accessToken);
|
|
77
|
+
const login = () => runKiroLoginFlowOnce({
|
|
78
|
+
login: {
|
|
79
|
+
...options.login,
|
|
80
|
+
useDeviceFlow: true,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
70
83
|
if (backend === "acp")
|
|
71
84
|
return new KiroAcpTransport(acpTransportOptions(options));
|
|
72
85
|
if (backend === "cli-chat") {
|
|
73
86
|
return new KiroCliChatTransport({
|
|
74
87
|
trustAllTools: options.trustAllTools,
|
|
75
88
|
...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
|
|
76
|
-
login
|
|
89
|
+
login,
|
|
77
90
|
});
|
|
78
91
|
}
|
|
79
92
|
if (backend === "fetch") {
|
|
@@ -81,11 +94,11 @@ function localTransport(options, accessToken) {
|
|
|
81
94
|
if (isKiroDeviceAuthKey(apiKey)) {
|
|
82
95
|
return new KiroRestTransport(fetchTransportOptions(options), {
|
|
83
96
|
credentialProvider: async () => (await credentialFromKiroDeviceAuthKey(apiKey).catch(() => undefined)) ?? readKiroCliSessionCredential(),
|
|
84
|
-
login
|
|
97
|
+
login,
|
|
85
98
|
});
|
|
86
99
|
}
|
|
87
100
|
return new KiroRestTransport(fetchTransportOptions(options, apiKey === "kiro-plugin-local-transport" ? undefined : apiKey), {
|
|
88
|
-
login
|
|
101
|
+
login,
|
|
89
102
|
});
|
|
90
103
|
}
|
|
91
104
|
return undefined;
|
|
@@ -116,8 +129,12 @@ export function createKiroPlugin() {
|
|
|
116
129
|
let configuredModels = { ...options.extraModels };
|
|
117
130
|
let userModelOverrides;
|
|
118
131
|
const disabledModels = new Set(options.disabledModels.map(normalizeModelName));
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
const stored = await loadStoredModelCache();
|
|
133
|
+
if (stored.models.length > 0) {
|
|
134
|
+
modelCache.update(mergeModelInfos(stored.models, options.extraModels), stored.updatedAt);
|
|
135
|
+
}
|
|
136
|
+
else if (Object.keys(options.extraModels).length > 0) {
|
|
137
|
+
modelCache.update(extraModelInfos(options.extraModels));
|
|
121
138
|
}
|
|
122
139
|
let discovery;
|
|
123
140
|
let lastModelDiscoveryAt = 0;
|
|
@@ -127,10 +144,13 @@ export function createKiroPlugin() {
|
|
|
127
144
|
return [];
|
|
128
145
|
}
|
|
129
146
|
if (!discovery) {
|
|
130
|
-
discovery =
|
|
147
|
+
discovery = discoverModelsFromCommand(options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
|
|
131
148
|
.then((models) => {
|
|
132
|
-
if (models.length > 0)
|
|
149
|
+
if (models.length > 0) {
|
|
133
150
|
lastModelDiscoveryAt = Date.now();
|
|
151
|
+
modelCache.update(mergeModelInfos(models, options.extraModels), lastModelDiscoveryAt);
|
|
152
|
+
void saveStoredModelCache(models, lastModelDiscoveryAt);
|
|
153
|
+
}
|
|
134
154
|
return models;
|
|
135
155
|
})
|
|
136
156
|
.catch(() => [])
|
|
@@ -138,7 +158,7 @@ export function createKiroPlugin() {
|
|
|
138
158
|
discovery = undefined;
|
|
139
159
|
});
|
|
140
160
|
}
|
|
141
|
-
return
|
|
161
|
+
return discovery;
|
|
142
162
|
};
|
|
143
163
|
const resolver = new ModelResolver({
|
|
144
164
|
cache: modelCache,
|
|
@@ -154,6 +174,10 @@ export function createKiroPlugin() {
|
|
|
154
174
|
const transport = localTransport(options, bearerToken(init));
|
|
155
175
|
return createKiroFetch({
|
|
156
176
|
resolver,
|
|
177
|
+
models: async () => {
|
|
178
|
+
await refreshModels(true).catch(() => []);
|
|
179
|
+
return Object.keys(visibleProviderModels(modelCache, configuredModels, options.hiddenModels, disabledModels));
|
|
180
|
+
},
|
|
157
181
|
...(transport ? { transport } : {}),
|
|
158
182
|
})(input, init);
|
|
159
183
|
};
|
|
@@ -191,6 +215,7 @@ export function createKiroPlugin() {
|
|
|
191
215
|
localServer = undefined;
|
|
192
216
|
},
|
|
193
217
|
config: async (config) => {
|
|
218
|
+
await refreshModels();
|
|
194
219
|
config.provider ??= {};
|
|
195
220
|
config.provider[options.providerID] ??= {};
|
|
196
221
|
const provider = config.provider[options.providerID];
|
|
@@ -112,9 +112,10 @@ Kiro 모델 목록은 tier, region, release timing에 따라 바뀔 수 있으
|
|
|
112
112
|
필수 동작:
|
|
113
113
|
|
|
114
114
|
- startup 시 model discovery를 시도합니다.
|
|
115
|
-
- discovery 결과는 TTL cache에 저장합니다.
|
|
116
|
-
-
|
|
117
|
-
-
|
|
115
|
+
- discovery 결과는 TTL cache와 local persistent cache에 저장합니다.
|
|
116
|
+
- refresh 실패 시 마지막 성공 model list cache를 유지합니다.
|
|
117
|
+
- 마지막 성공 cache도 없으면 `auto` placeholder만 표시합니다.
|
|
118
|
+
- static preset을 사용 가능 모델처럼 표시하지 않고, 사용자 `extraModels`와 pass-through만 허용합니다.
|
|
118
119
|
- 사용자가 `hiddenModels`, `modelAliases`, `disabledModels`를 설정할 수 있게 합니다.
|
|
119
120
|
- 알 수 없는 모델은 기본적으로 pass-through합니다.
|
|
120
121
|
- Kiro가 거절한 경우 같은 model family의 후보를 제안합니다.
|