@bogyie/opencode-kiro-plugin 0.1.0

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +21 -0
  3. package/README.md +187 -0
  4. package/dist/acp-client.d.ts +57 -0
  5. package/dist/acp-client.js +171 -0
  6. package/dist/acp-transport.d.ts +33 -0
  7. package/dist/acp-transport.js +358 -0
  8. package/dist/auth.d.ts +25 -0
  9. package/dist/auth.js +77 -0
  10. package/dist/cli-transport.d.ts +17 -0
  11. package/dist/cli-transport.js +52 -0
  12. package/dist/config.d.ts +27 -0
  13. package/dist/config.js +76 -0
  14. package/dist/errors.d.ts +10 -0
  15. package/dist/errors.js +63 -0
  16. package/dist/fetch-adapter.d.ts +13 -0
  17. package/dist/fetch-adapter.js +35 -0
  18. package/dist/index.d.ts +24 -0
  19. package/dist/index.js +20 -0
  20. package/dist/kiro-cli.d.ts +8 -0
  21. package/dist/kiro-cli.js +16 -0
  22. package/dist/kiro-transport.d.ts +33 -0
  23. package/dist/kiro-transport.js +250 -0
  24. package/dist/model-cache.d.ts +19 -0
  25. package/dist/model-cache.js +33 -0
  26. package/dist/model-discovery.d.ts +5 -0
  27. package/dist/model-discovery.js +76 -0
  28. package/dist/model-resolver.d.ts +29 -0
  29. package/dist/model-resolver.js +79 -0
  30. package/dist/models.d.ts +14 -0
  31. package/dist/models.js +70 -0
  32. package/dist/plugin.d.ts +8 -0
  33. package/dist/plugin.js +188 -0
  34. package/dist/request-adapter.d.ts +93 -0
  35. package/dist/request-adapter.js +221 -0
  36. package/dist/response-adapter.d.ts +25 -0
  37. package/dist/response-adapter.js +108 -0
  38. package/docs/community-implementations.md +59 -0
  39. package/docs/e2e-validation.md +196 -0
  40. package/docs/implementation-plan.md +309 -0
  41. package/docs/implementation-strategy.md +241 -0
  42. package/docs/kiro-integration-notes.md +129 -0
  43. package/docs/opencode-provider-notes.md +87 -0
  44. package/docs/references.md +50 -0
  45. package/docs/research-summary.md +69 -0
  46. package/examples/opencode.jsonc +47 -0
  47. package/package.json +67 -0
@@ -0,0 +1,79 @@
1
+ export class ModelResolutionError extends Error {
2
+ model;
3
+ suggestions;
4
+ constructor(message, model, suggestions) {
5
+ super(message);
6
+ this.model = model;
7
+ this.suggestions = suggestions;
8
+ this.name = "ModelResolutionError";
9
+ }
10
+ }
11
+ export function normalizeModelName(input) {
12
+ let name = input.trim();
13
+ if (!name)
14
+ return name;
15
+ name = name.replace(/\[\d+[mk]\]$/i, "").toLowerCase();
16
+ const inverted = /^claude-(\d+)\.(\d+)-(haiku|sonnet|opus)-.+$/.exec(name);
17
+ if (inverted)
18
+ return `claude-${inverted[3]}-${inverted[1]}.${inverted[2]}`;
19
+ name = name.replace(/-(thinking|low|medium|high|max)$/, "");
20
+ const standardMinor = /^(claude-(?:haiku|sonnet|opus)-\d+)-(\d{1,2})(?:-(?:\d{8}|latest|\d+))?$/.exec(name);
21
+ if (standardMinor)
22
+ return `${standardMinor[1]}.${standardMinor[2]}`;
23
+ const standardMajor = /^(claude-(?:haiku|sonnet|opus)-\d+)(?:-\d{8})?$/.exec(name);
24
+ if (standardMajor)
25
+ return standardMajor[1] ?? name;
26
+ const legacy = /^claude-(\d+)-(\d+)-(haiku|sonnet|opus)(?:-(?:\d{8}|latest|\d+))?$/.exec(name);
27
+ if (legacy)
28
+ return `claude-${legacy[1]}.${legacy[2]}-${legacy[3]}`;
29
+ const dotWithDate = /^(claude-(?:(?:haiku|sonnet|opus)-\d+\.\d+|\d+\.\d+-(?:haiku|sonnet|opus)))-\d{8}$/.exec(name);
30
+ if (dotWithDate)
31
+ return dotWithDate[1] ?? name;
32
+ return name;
33
+ }
34
+ function familyOf(model) {
35
+ return /(haiku|sonnet|opus)/i.exec(model)?.[1]?.toLowerCase();
36
+ }
37
+ export class ModelResolver {
38
+ #cache;
39
+ #aliases;
40
+ #hiddenModels;
41
+ #disabledModels;
42
+ #disablePassThrough;
43
+ constructor(options) {
44
+ this.#cache = options.cache;
45
+ this.#aliases = options.aliases ?? {};
46
+ this.#hiddenModels = options.hiddenModels ?? {};
47
+ this.#disabledModels = new Set((options.disabledModels ?? []).map(normalizeModelName));
48
+ this.#disablePassThrough = options.disablePassThrough === true;
49
+ }
50
+ resolve(model) {
51
+ const aliased = this.#aliases[model] ?? this.#aliases[normalizeModelName(model)] ?? model;
52
+ const normalized = normalizeModelName(aliased);
53
+ if (this.#disabledModels.has(normalized)) {
54
+ throw new ModelResolutionError(`Model is disabled: ${model}`, model, this.suggestions(model));
55
+ }
56
+ if (this.#cache.has(normalized)) {
57
+ return { internalID: normalized, source: "cache", original: model, normalized, verified: true };
58
+ }
59
+ const hidden = this.#hiddenModels[normalized];
60
+ if (hidden) {
61
+ return { internalID: hidden, source: "hidden", original: model, normalized, verified: true };
62
+ }
63
+ if (this.#disablePassThrough) {
64
+ throw new ModelResolutionError(`Unsupported model: ${model}`, model, this.suggestions(model));
65
+ }
66
+ return { internalID: normalized, source: "passthrough", original: model, normalized, verified: false };
67
+ }
68
+ availableModels() {
69
+ return [...new Set([...this.#cache.ids(), ...Object.keys(this.#hiddenModels), ...Object.keys(this.#aliases)])].sort();
70
+ }
71
+ suggestions(model) {
72
+ const family = familyOf(model);
73
+ const available = this.availableModels();
74
+ if (!family)
75
+ return available;
76
+ const matching = available.filter((item) => familyOf(item) === family);
77
+ return matching.length > 0 ? matching : available;
78
+ }
79
+ }
@@ -0,0 +1,14 @@
1
+ export interface ProviderModelConfig {
2
+ readonly name?: string;
3
+ readonly limit?: {
4
+ readonly context: number;
5
+ readonly output: number;
6
+ };
7
+ readonly modalities?: {
8
+ readonly input: ReadonlyArray<"text" | "image" | "pdf">;
9
+ readonly output: ReadonlyArray<"text">;
10
+ };
11
+ readonly tool_call?: boolean;
12
+ readonly variants?: Readonly<Record<string, Record<string, unknown>>>;
13
+ }
14
+ export declare const FALLBACK_MODELS: Readonly<Record<string, ProviderModelConfig>>;
package/dist/models.js ADDED
@@ -0,0 +1,70 @@
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-sonnet-4": {
11
+ name: "Claude Sonnet 4",
12
+ limit: { context: 200_000, output: 64_000 },
13
+ modalities: TEXT_IMAGE_PDF,
14
+ tool_call: true,
15
+ },
16
+ "claude-sonnet-4-5": {
17
+ name: "Claude Sonnet 4.5",
18
+ limit: { context: 200_000, output: 64_000 },
19
+ modalities: TEXT_IMAGE_PDF,
20
+ tool_call: true,
21
+ },
22
+ "claude-sonnet-4-6": {
23
+ name: "Claude Sonnet 4.6",
24
+ limit: { context: 1_000_000, output: 64_000 },
25
+ modalities: TEXT_IMAGE_PDF,
26
+ tool_call: true,
27
+ },
28
+ "claude-opus-4-8": {
29
+ name: "Claude Opus 4.8",
30
+ limit: { context: 1_000_000, output: 64_000 },
31
+ modalities: TEXT_IMAGE_PDF,
32
+ tool_call: true,
33
+ },
34
+ "claude-haiku-4-5": {
35
+ name: "Claude Haiku 4.5",
36
+ limit: { context: 200_000, output: 64_000 },
37
+ modalities: { input: ["text", "image"], output: ["text"] },
38
+ tool_call: true,
39
+ },
40
+ "deepseek-3.2": {
41
+ name: "DeepSeek 3.2",
42
+ limit: { context: 128_000, output: 64_000 },
43
+ modalities: TEXT_ONLY,
44
+ tool_call: true,
45
+ },
46
+ "glm-5": {
47
+ name: "GLM-5",
48
+ limit: { context: 200_000, output: 64_000 },
49
+ modalities: TEXT_ONLY,
50
+ tool_call: true,
51
+ },
52
+ "minimax-m2.5": {
53
+ name: "MiniMax M2.5",
54
+ limit: { context: 200_000, output: 64_000 },
55
+ modalities: TEXT_ONLY,
56
+ tool_call: true,
57
+ },
58
+ "minimax-m2.1": {
59
+ name: "MiniMax M2.1",
60
+ limit: { context: 200_000, output: 64_000 },
61
+ modalities: TEXT_ONLY,
62
+ tool_call: true,
63
+ },
64
+ "qwen3-coder-next": {
65
+ name: "Qwen3 Coder Next",
66
+ limit: { context: 256_000, output: 64_000 },
67
+ modalities: TEXT_ONLY,
68
+ tool_call: true,
69
+ },
70
+ };
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import type { KiroAcpTransportOptions } from "./acp-transport.js";
3
+ import type { KiroPluginOptions } from "./config.js";
4
+ import type { KiroTransportOptions } from "./kiro-transport.js";
5
+ export declare function acpTransportOptions(options: Pick<KiroPluginOptions, "requestTimeoutMs" | "trustAllTools">): KiroAcpTransportOptions;
6
+ export declare function fetchTransportOptions(options: Pick<KiroPluginOptions, "region" | "endpoint" | "profileArn" | "userAgent" | "agentMode" | "maxAttempts" | "requestTimeoutMs">, accessToken: string): KiroTransportOptions;
7
+ export declare function createKiroPlugin(): Plugin;
8
+ export declare const KiroPlugin: Plugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,188 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { KiroAcpTransport } from "./acp-transport.js";
3
+ import { detectAuth, resolveApiKey } from "./auth.js";
4
+ import { KiroCliChatTransport } from "./cli-transport.js";
5
+ import { loadOptions } from "./config.js";
6
+ import { createKiroFetch } from "./fetch-adapter.js";
7
+ import { ModelCache } from "./model-cache.js";
8
+ import { refreshModelCacheFromCommand } from "./model-discovery.js";
9
+ import { ModelResolver, normalizeModelName } from "./model-resolver.js";
10
+ 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
+ function discoveredProviderModels(cache) {
20
+ return Object.fromEntries(cache.all().map((model) => [
21
+ model.id,
22
+ {
23
+ name: model.name ?? model.id,
24
+ },
25
+ ]));
26
+ }
27
+ export function acpTransportOptions(options) {
28
+ return {
29
+ trustAllTools: options.trustAllTools,
30
+ ...(options.requestTimeoutMs ? { promptTimeoutMs: options.requestTimeoutMs } : {}),
31
+ };
32
+ }
33
+ export function fetchTransportOptions(options, accessToken) {
34
+ return {
35
+ region: options.region,
36
+ accessToken,
37
+ maxAttempts: options.maxAttempts,
38
+ ...(options.endpoint ? { endpoint: options.endpoint } : {}),
39
+ ...(options.profileArn ? { profileArn: options.profileArn } : {}),
40
+ ...(options.userAgent ? { userAgent: options.userAgent } : {}),
41
+ ...(options.agentMode ? { agentMode: options.agentMode } : {}),
42
+ ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
43
+ };
44
+ }
45
+ export function createKiroPlugin() {
46
+ return async (_input, rawOptions) => {
47
+ const options = loadOptions(rawOptions);
48
+ const baseURL = options.endpoint ?? `https://q.${options.region}.amazonaws.com`;
49
+ const modelCache = new ModelCache(options.modelCacheTtlSeconds);
50
+ modelCache.update([...Object.keys(FALLBACK_MODELS), ...Object.keys(options.extraModels)].map((id) => ({ id: normalizeModelName(id) })));
51
+ let discovery;
52
+ let discoveryAttempted = false;
53
+ const discoverIfStale = async () => {
54
+ if (options.modelDiscovery === "off" ||
55
+ options.modelDiscoveryCommand.length === 0 ||
56
+ (discoveryAttempted && !modelCache.isStale())) {
57
+ return;
58
+ }
59
+ discoveryAttempted = true;
60
+ discovery ??= refreshModelCacheFromCommand(modelCache, options.modelDiscoveryCommand[0], options.modelDiscoveryCommand.slice(1))
61
+ .catch(() => undefined)
62
+ .then(() => {
63
+ discovery = undefined;
64
+ });
65
+ await discovery;
66
+ };
67
+ const resolver = new ModelResolver({
68
+ cache: modelCache,
69
+ aliases: options.modelAliases,
70
+ hiddenModels: options.hiddenModels,
71
+ disabledModels: options.disabledModels,
72
+ disablePassThrough: options.disableModelPassThrough,
73
+ });
74
+ return {
75
+ config: async (config) => {
76
+ config.provider ??= {};
77
+ config.provider[options.providerID] ??= {};
78
+ const provider = config.provider[options.providerID];
79
+ provider.name ??= "Kiro";
80
+ provider.npm = "@ai-sdk/openai-compatible";
81
+ provider.api ??= baseURL;
82
+ provider.options ??= {};
83
+ provider.models = mergeModels(options.extraModels, provider.models);
84
+ },
85
+ auth: {
86
+ provider: options.providerID,
87
+ methods: [
88
+ {
89
+ type: "api",
90
+ label: "Kiro API key",
91
+ prompts: [
92
+ {
93
+ type: "text",
94
+ key: "apiKey",
95
+ message: "Enter KIRO_API_KEY",
96
+ placeholder: "ksk_...",
97
+ },
98
+ ],
99
+ authorize: async (inputs) => {
100
+ const key = inputs?.apiKey;
101
+ if (!key)
102
+ return { type: "failed" };
103
+ return { type: "success", key };
104
+ },
105
+ },
106
+ ],
107
+ loader: async (auth) => {
108
+ await discoverIfStale();
109
+ const apiKey = await resolveApiKey(auth);
110
+ const transport = options.backend === "acp"
111
+ ? new KiroAcpTransport(acpTransportOptions(options))
112
+ : options.backend === "cli-chat"
113
+ ? new KiroCliChatTransport({
114
+ trustAllTools: options.trustAllTools,
115
+ ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
116
+ })
117
+ : apiKey
118
+ ? new CodeWhispererKiroTransport(fetchTransportOptions(options, apiKey))
119
+ : options.backend === "auto"
120
+ ? new KiroCliChatTransport({
121
+ trustAllTools: options.trustAllTools,
122
+ ...(options.requestTimeoutMs ? { requestTimeoutMs: options.requestTimeoutMs } : {}),
123
+ })
124
+ : undefined;
125
+ return {
126
+ apiKey,
127
+ baseURL,
128
+ fetch: createKiroFetch({
129
+ resolver,
130
+ ...(transport ? { transport } : {}),
131
+ }),
132
+ };
133
+ },
134
+ },
135
+ tool: {
136
+ kiro_status: tool({
137
+ description: "Show Kiro plugin backend, auth, region, and model fallback status.",
138
+ args: {},
139
+ execute: async () => {
140
+ const auth = await detectAuth();
141
+ return {
142
+ title: "Kiro status",
143
+ output: [
144
+ `provider: ${options.providerID}`,
145
+ `backend: ${options.backend}`,
146
+ `region: ${auth.region}`,
147
+ `auth: ${auth.method}`,
148
+ `authenticated: ${auth.authenticated ? "yes" : "no"}`,
149
+ `models: ${Object.keys(FALLBACK_MODELS).length} fallback presets`,
150
+ auth.message,
151
+ ].join("\n"),
152
+ metadata: {
153
+ providerID: options.providerID,
154
+ backend: options.backend,
155
+ region: auth.region,
156
+ authMethod: auth.method,
157
+ authenticated: auth.authenticated,
158
+ },
159
+ };
160
+ },
161
+ }),
162
+ },
163
+ provider: {
164
+ id: options.providerID,
165
+ models: async (provider) => {
166
+ await discoverIfStale();
167
+ const models = {
168
+ ...discoveredProviderModels(modelCache),
169
+ ...(provider.models ?? {}),
170
+ };
171
+ return Object.fromEntries(Object.entries(models).map(([id, model]) => [
172
+ id,
173
+ {
174
+ ...model,
175
+ api: {
176
+ ...(model.api ?? {}),
177
+ id,
178
+ npm: "@ai-sdk/openai-compatible",
179
+ url: baseURL,
180
+ },
181
+ },
182
+ ]));
183
+ },
184
+ },
185
+ };
186
+ };
187
+ }
188
+ export const KiroPlugin = createKiroPlugin();
@@ -0,0 +1,93 @@
1
+ import type { ModelResolver } from "./model-resolver.js";
2
+ export interface OpenAIChatMessage {
3
+ readonly role: "system" | "user" | "assistant" | "tool";
4
+ readonly content?: string | ReadonlyArray<OpenAIContentPart>;
5
+ readonly tool_call_id?: string;
6
+ readonly tool_calls?: ReadonlyArray<OpenAIMessageToolCall>;
7
+ readonly name?: string;
8
+ }
9
+ export type OpenAIContentPart = {
10
+ type: string;
11
+ text?: string;
12
+ [key: string]: unknown;
13
+ };
14
+ export interface OpenAIMessageToolCall {
15
+ readonly id?: string;
16
+ readonly type?: "function";
17
+ readonly function?: {
18
+ readonly name?: string;
19
+ readonly arguments?: string;
20
+ };
21
+ }
22
+ export interface OpenAIChatRequest {
23
+ readonly model: string;
24
+ readonly messages: ReadonlyArray<OpenAIChatMessage>;
25
+ readonly stream?: boolean;
26
+ readonly tools?: ReadonlyArray<OpenAITool>;
27
+ readonly temperature?: number;
28
+ readonly max_tokens?: number;
29
+ readonly max_completion_tokens?: number;
30
+ readonly reasoning_effort?: string;
31
+ readonly reasoning?: {
32
+ readonly effort?: string;
33
+ };
34
+ readonly thinking?: {
35
+ readonly effort?: string;
36
+ };
37
+ }
38
+ export interface OpenAITool {
39
+ readonly type: "function";
40
+ readonly function: {
41
+ readonly name: string;
42
+ readonly description?: string;
43
+ readonly parameters?: unknown;
44
+ };
45
+ }
46
+ export interface KiroGenerateRequest {
47
+ readonly modelId: string;
48
+ readonly prompt: string;
49
+ readonly system?: string;
50
+ readonly history: ReadonlyArray<KiroConversationTurn>;
51
+ readonly tools: ReadonlyArray<KiroToolSpec>;
52
+ readonly toolResults: ReadonlyArray<KiroToolResult>;
53
+ readonly images: ReadonlyArray<KiroImageBlock>;
54
+ readonly documents: ReadonlyArray<KiroDocumentBlock>;
55
+ readonly modelOptions: KiroModelOptions;
56
+ readonly stream: boolean;
57
+ readonly metadata: {
58
+ readonly originalModel: string;
59
+ readonly normalizedModel: string;
60
+ readonly modelSource: string;
61
+ readonly hasTools: boolean;
62
+ };
63
+ }
64
+ export interface KiroModelOptions {
65
+ readonly temperature?: number;
66
+ readonly maxTokens?: number;
67
+ readonly reasoningEffort?: string;
68
+ }
69
+ export interface KiroImageBlock {
70
+ readonly format: "png" | "jpeg" | "gif" | "webp";
71
+ readonly bytes: Uint8Array;
72
+ }
73
+ export interface KiroDocumentBlock {
74
+ readonly name: string;
75
+ readonly format: "pdf" | "txt" | "md" | "csv" | "html" | "doc" | "docx" | "xls" | "xlsx";
76
+ readonly bytes: Uint8Array;
77
+ }
78
+ export interface KiroConversationTurn {
79
+ readonly role: "user" | "assistant" | "tool";
80
+ readonly content: string;
81
+ }
82
+ export interface KiroToolSpec {
83
+ readonly name: string;
84
+ readonly description?: string;
85
+ readonly inputSchema: unknown;
86
+ }
87
+ export interface KiroToolResult {
88
+ readonly toolUseId: string;
89
+ readonly content: string;
90
+ readonly toolName?: string;
91
+ }
92
+ export declare function readOpenAIRequest(input: RequestInfo | URL, init?: RequestInit): Promise<OpenAIChatRequest>;
93
+ export declare function toKiroGenerateRequest(request: OpenAIChatRequest, resolver: ModelResolver): KiroGenerateRequest;
@@ -0,0 +1,221 @@
1
+ function textFromContent(content) {
2
+ if (content === undefined)
3
+ return "";
4
+ if (typeof content === "string")
5
+ return content;
6
+ return content
7
+ .map((part) => {
8
+ if (part.type === "text" && typeof part.text === "string")
9
+ return part.text;
10
+ return "";
11
+ })
12
+ .filter(Boolean)
13
+ .join("\n");
14
+ }
15
+ function dataUrlBytes(dataUrl) {
16
+ const match = /^data:([^;,]+);base64,(.+)$/i.exec(dataUrl);
17
+ if (!match?.[1] || !match[2])
18
+ return undefined;
19
+ return {
20
+ mime: match[1].toLowerCase(),
21
+ bytes: Uint8Array.from(Buffer.from(match[2], "base64")),
22
+ };
23
+ }
24
+ function imageFormat(mime) {
25
+ if (mime === "image/png")
26
+ return "png";
27
+ if (mime === "image/jpeg" || mime === "image/jpg")
28
+ return "jpeg";
29
+ if (mime === "image/gif")
30
+ return "gif";
31
+ if (mime === "image/webp")
32
+ return "webp";
33
+ return undefined;
34
+ }
35
+ function documentFormat(mime) {
36
+ if (mime === "application/pdf")
37
+ return "pdf";
38
+ if (mime === "text/plain")
39
+ return "txt";
40
+ if (mime === "text/markdown")
41
+ return "md";
42
+ if (mime === "text/csv")
43
+ return "csv";
44
+ if (mime === "text/html")
45
+ return "html";
46
+ return undefined;
47
+ }
48
+ function partUrl(part) {
49
+ if (part.type === "image_url") {
50
+ const image = part.image_url;
51
+ return typeof image?.url === "string" ? image.url : undefined;
52
+ }
53
+ if (part.type === "input_image") {
54
+ if (typeof part.image_url === "string")
55
+ return part.image_url;
56
+ const image = part.image_url;
57
+ return typeof image?.url === "string" ? image.url : undefined;
58
+ }
59
+ if (part.type === "file" || part.type === "input_file") {
60
+ const file = part.file;
61
+ if (typeof file?.file_data === "string")
62
+ return file.file_data;
63
+ if (typeof part.file_data === "string")
64
+ return part.file_data;
65
+ }
66
+ return undefined;
67
+ }
68
+ function partFilename(part, fallback) {
69
+ const file = part.file;
70
+ if (typeof file?.filename === "string" && file.filename)
71
+ return file.filename;
72
+ if (typeof part.filename === "string" && part.filename)
73
+ return part.filename;
74
+ return fallback;
75
+ }
76
+ function mediaFromContent(content) {
77
+ if (!Array.isArray(content))
78
+ return { images: [], documents: [] };
79
+ const images = [];
80
+ const documents = [];
81
+ for (const [index, part] of content.entries()) {
82
+ const url = partUrl(part);
83
+ if (!url)
84
+ continue;
85
+ const decoded = dataUrlBytes(url);
86
+ if (!decoded)
87
+ continue;
88
+ const imgFormat = imageFormat(decoded.mime);
89
+ if (imgFormat) {
90
+ images.push({ format: imgFormat, bytes: decoded.bytes });
91
+ continue;
92
+ }
93
+ const docFormat = documentFormat(decoded.mime);
94
+ if (docFormat) {
95
+ documents.push({
96
+ name: partFilename(part, `attachment-${index + 1}.${docFormat}`),
97
+ format: docFormat,
98
+ bytes: decoded.bytes,
99
+ });
100
+ }
101
+ }
102
+ return { images, documents };
103
+ }
104
+ function toolSpecs(tools) {
105
+ return (tools ?? []).map((tool) => ({
106
+ name: tool.function.name,
107
+ ...(tool.function.description ? { description: tool.function.description } : {}),
108
+ inputSchema: tool.function.parameters ?? { type: "object", properties: {} },
109
+ }));
110
+ }
111
+ function requestedToolCallIds(messages) {
112
+ const ids = new Set();
113
+ for (const message of messages) {
114
+ if (message.role !== "assistant")
115
+ continue;
116
+ for (const toolCall of message.tool_calls ?? []) {
117
+ if (toolCall.id)
118
+ ids.add(toolCall.id);
119
+ }
120
+ }
121
+ return ids;
122
+ }
123
+ function toolNameById(messages) {
124
+ const names = new Map();
125
+ for (const message of messages) {
126
+ if (message.role !== "assistant")
127
+ continue;
128
+ for (const toolCall of message.tool_calls ?? []) {
129
+ if (toolCall.id && toolCall.function?.name)
130
+ names.set(toolCall.id, toolCall.function.name);
131
+ }
132
+ }
133
+ return names;
134
+ }
135
+ function toolResults(messages) {
136
+ const requested = requestedToolCallIds(messages);
137
+ const names = toolNameById(messages);
138
+ const results = new Map();
139
+ for (const message of messages) {
140
+ if (message.role !== "tool" || !message.tool_call_id)
141
+ continue;
142
+ if (requested.size > 0 && !requested.has(message.tool_call_id))
143
+ continue;
144
+ const toolName = message.name ?? names.get(message.tool_call_id);
145
+ results.set(message.tool_call_id, {
146
+ toolUseId: message.tool_call_id,
147
+ content: textFromContent(message.content),
148
+ ...(toolName ? { toolName } : {}),
149
+ });
150
+ }
151
+ return [...results.values()];
152
+ }
153
+ function finiteNumber(value) {
154
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
155
+ }
156
+ function positiveInteger(value) {
157
+ const number = finiteNumber(value);
158
+ if (number === undefined || number < 1)
159
+ return undefined;
160
+ return Math.floor(number);
161
+ }
162
+ function nonEmptyString(value) {
163
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
164
+ }
165
+ function modelOptions(request) {
166
+ const temperature = finiteNumber(request.temperature);
167
+ const maxTokens = positiveInteger(request.max_completion_tokens) ?? positiveInteger(request.max_tokens);
168
+ const reasoningEffort = nonEmptyString(request.reasoning_effort) ?? nonEmptyString(request.reasoning?.effort) ?? nonEmptyString(request.thinking?.effort);
169
+ return {
170
+ ...(temperature !== undefined ? { temperature } : {}),
171
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
172
+ ...(reasoningEffort ? { reasoningEffort } : {}),
173
+ };
174
+ }
175
+ export async function readOpenAIRequest(input, init) {
176
+ if (init?.body) {
177
+ const raw = typeof init.body === "string" ? init.body : new TextDecoder().decode(init.body);
178
+ return JSON.parse(raw);
179
+ }
180
+ if (input instanceof Request) {
181
+ return (await input.clone().json());
182
+ }
183
+ throw new Error("Missing OpenAI-compatible request body");
184
+ }
185
+ export function toKiroGenerateRequest(request, resolver) {
186
+ const resolved = resolver.resolve(request.model);
187
+ const system = request.messages
188
+ .filter((message) => message.role === "system")
189
+ .map((message) => textFromContent(message.content))
190
+ .filter(Boolean)
191
+ .join("\n\n");
192
+ const turns = request.messages
193
+ .filter((message) => message.role !== "system")
194
+ .map((message) => {
195
+ const content = textFromContent(message.content);
196
+ return content ? { role: message.role, content } : undefined;
197
+ })
198
+ .filter((item) => item !== undefined);
199
+ const current = turns.at(-1);
200
+ const history = turns.slice(0, -1);
201
+ const currentMessage = request.messages.filter((message) => message.role !== "system").at(-1);
202
+ const media = mediaFromContent(currentMessage?.content);
203
+ return {
204
+ modelId: resolved.internalID,
205
+ prompt: current?.content ?? "",
206
+ history,
207
+ tools: toolSpecs(request.tools),
208
+ toolResults: toolResults(request.messages),
209
+ images: media.images,
210
+ documents: media.documents,
211
+ modelOptions: modelOptions(request),
212
+ ...(system ? { system } : {}),
213
+ stream: request.stream === true,
214
+ metadata: {
215
+ originalModel: request.model,
216
+ normalizedModel: resolved.normalized,
217
+ modelSource: resolved.source,
218
+ hasTools: Boolean(request.tools?.length),
219
+ },
220
+ };
221
+ }