@javargasm/opencode-kiro-auth 0.3.0 → 0.4.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.
package/dist/index.js CHANGED
@@ -16,8 +16,121 @@ var __export = (target, all) => {
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
18
 
19
- // src/debug.ts
19
+ // src/file-logger.ts
20
20
  import { appendFileSync, mkdirSync } from "node:fs";
21
+ import { AsyncLocalStorage } from "node:async_hooks";
22
+ function isEnabled() {
23
+ return true;
24
+ }
25
+ function ensureLogDir() {
26
+ if (dirEnsured)
27
+ return;
28
+ try {
29
+ mkdirSync(LOG_DIR, { recursive: true });
30
+ dirEnsured = true;
31
+ } catch {}
32
+ }
33
+ function sanitizeSessionId(id) {
34
+ const slug = id.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
35
+ return slug.length > 0 ? slug : "default";
36
+ }
37
+ function currentSessionLogFile() {
38
+ return sessionLogStore.getStore()?.file ?? null;
39
+ }
40
+ function enterSessionLog(sessionId) {
41
+ const id = sanitizeSessionId(sessionId ?? "default");
42
+ sessionLogStore.enterWith({ file: `${LOG_DIR}/session-${id}.log`, sessionId: id });
43
+ return id;
44
+ }
45
+ function writeLine(file, data) {
46
+ if (!isEnabled())
47
+ return;
48
+ ensureLogDir();
49
+ const entry = {
50
+ ts: new Date().toISOString(),
51
+ ...data
52
+ };
53
+ try {
54
+ appendFileSync(file, JSON.stringify(entry) + `
55
+ `);
56
+ } catch {}
57
+ }
58
+ function createSessionLogger(sessionId) {
59
+ const id = sanitizeSessionId(sessionId ?? "default");
60
+ const file = `${LOG_DIR}/session-${id}.log`;
61
+ const emit = (data) => writeLine(file, { sessionId: id, ...data });
62
+ return {
63
+ file,
64
+ sessionId: id,
65
+ logRequest(meta, requestBody) {
66
+ emit({
67
+ type: "request",
68
+ ...meta,
69
+ body: safeParseJson(requestBody)
70
+ });
71
+ },
72
+ logResponseEvent(event) {
73
+ emit({
74
+ type: "response_event",
75
+ eventType: event.type,
76
+ seq: event.eventSeq,
77
+ data: event.data
78
+ });
79
+ },
80
+ logResponseDone(meta) {
81
+ emit({ type: "response_done", ...meta });
82
+ },
83
+ logHttpError(meta) {
84
+ emit({ type: "http_error", ...meta });
85
+ },
86
+ logStreamError(meta) {
87
+ emit({ type: "stream_error", ...meta });
88
+ },
89
+ logCaughtError(meta, error) {
90
+ emit({
91
+ type: "caught_error",
92
+ ...meta,
93
+ error: error === undefined ? undefined : serializeError(error)
94
+ });
95
+ }
96
+ };
97
+ }
98
+ function safeParseJson(s) {
99
+ try {
100
+ return JSON.parse(s);
101
+ } catch {
102
+ return s;
103
+ }
104
+ }
105
+ function serializeError(error, depth = 0) {
106
+ if (depth > 5)
107
+ return "[cause chain truncated]";
108
+ if (error instanceof Error) {
109
+ const out = {
110
+ name: error.name,
111
+ message: error.message,
112
+ stack: error.stack
113
+ };
114
+ if (error.cause !== undefined) {
115
+ out.cause = serializeError(error.cause, depth + 1);
116
+ }
117
+ for (const key of Object.keys(error)) {
118
+ if (!(key in out))
119
+ out[key] = error[key];
120
+ }
121
+ return out;
122
+ }
123
+ if (error && typeof error === "object")
124
+ return error;
125
+ return String(error);
126
+ }
127
+ var LOG_DIR = "/tmp/kiro-logs", dirEnsured = false, sessionLogStore;
128
+ var init_file_logger = __esm(() => {
129
+ sessionLogStore = new AsyncLocalStorage;
130
+ });
131
+
132
+ // src/debug.ts
133
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
21
134
  import { dirname, isAbsolute, resolve } from "node:path";
22
135
  function currentLevel() {
23
136
  const raw = (globalThis.process?.env?.KIRO_LOG ?? "").toLowerCase();
@@ -29,6 +142,9 @@ function enabled(level) {
29
142
  return LEVEL_ORDER[level] <= LEVEL_ORDER[currentLevel()];
30
143
  }
31
144
  function currentFilePath() {
145
+ const sessionFile = currentSessionLogFile();
146
+ if (sessionFile)
147
+ return sessionFile;
32
148
  const raw = globalThis.process?.env?.KIRO_LOG_FILE;
33
149
  if (!raw)
34
150
  return null;
@@ -38,10 +154,10 @@ function writeToFile(filePath, line) {
38
154
  try {
39
155
  const dir = dirname(filePath);
40
156
  if (!ensuredDirs.has(dir)) {
41
- mkdirSync(dir, { recursive: true });
157
+ mkdirSync2(dir, { recursive: true });
42
158
  ensuredDirs.add(dir);
43
159
  }
44
- appendFileSync(filePath, line + `
160
+ appendFileSync2(filePath, line + `
45
161
  `);
46
162
  } catch (err) {
47
163
  if (!fileFallbackWarned) {
@@ -101,6 +217,7 @@ function previewChunk(s) {
101
217
  }
102
218
  var LEVEL_ORDER, ensuredDirs, fileFallbackWarned = false, log, CHUNK_PREVIEW_LIMIT = 2048;
103
219
  var init_debug = __esm(() => {
220
+ init_file_logger();
104
221
  LEVEL_ORDER = {
105
222
  error: 0,
106
223
  warn: 1,
@@ -167,7 +284,7 @@ async function resolveProfileArn(accessToken, apiRegion) {
167
284
  Authorization: `Bearer ${accessToken}`,
168
285
  "Content-Type": "application/x-amz-json-1.0",
169
286
  "X-Amz-Target": "AmazonCodeWhispererService.ListAvailableProfiles",
170
- "user-agent": "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererruntime/0.1.16551 os/macos lang/rust/1.92.0 md/appVersion-2.7.1 app/AmazonQ-For-CLI"
287
+ "user-agent": "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererruntime/0.1.16551 os/macos lang/rust/1.92.0 md/appVersion-2.8.1 app/AmazonQ-For-CLI"
171
288
  },
172
289
  body: "{}"
173
290
  });
@@ -190,7 +307,7 @@ async function fetchAvailableModels(accessToken, apiRegion, profileArn) {
190
307
  Authorization: `Bearer ${accessToken}`,
191
308
  "Content-Type": "application/x-amz-json-1.0",
192
309
  "X-Amz-Target": "AmazonCodeWhispererService.ListAvailableModels",
193
- "user-agent": "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererruntime/0.1.16551 os/macos lang/rust/1.92.0 md/appVersion-2.7.1 app/AmazonQ-For-CLI"
310
+ "user-agent": "aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererruntime/0.1.16551 os/macos lang/rust/1.92.0 md/appVersion-2.8.1 app/AmazonQ-For-CLI"
194
311
  },
195
312
  body: "{}"
196
313
  });
@@ -293,7 +410,7 @@ var init_models = __esm(() => {
293
410
  {
294
411
  ...KIRO_DEFAULTS,
295
412
  id: "claude-fable-5",
296
- name: "Claude Fable 5",
413
+ name: "Claude Fable 5 (disabled)",
297
414
  reasoning: true,
298
415
  input: MULTIMODAL,
299
416
  contextWindow: 1e6,
@@ -364,7 +481,7 @@ var init_models = __esm(() => {
364
481
  reasoning: true,
365
482
  input: MULTIMODAL,
366
483
  contextWindow: 200000,
367
- maxTokens: 65536
484
+ maxTokens: 64000
368
485
  },
369
486
  {
370
487
  ...KIRO_DEFAULTS,
@@ -373,7 +490,7 @@ var init_models = __esm(() => {
373
490
  reasoning: true,
374
491
  input: MULTIMODAL,
375
492
  contextWindow: 200000,
376
- maxTokens: 65536
493
+ maxTokens: 64000
377
494
  },
378
495
  {
379
496
  ...KIRO_DEFAULTS,
@@ -382,7 +499,7 @@ var init_models = __esm(() => {
382
499
  reasoning: false,
383
500
  input: MULTIMODAL,
384
501
  contextWindow: 200000,
385
- maxTokens: 65536
502
+ maxTokens: 64000
386
503
  },
387
504
  {
388
505
  ...KIRO_DEFAULTS,
@@ -417,8 +534,8 @@ var init_models = __esm(() => {
417
534
  name: "Auto",
418
535
  reasoning: true,
419
536
  input: MULTIMODAL,
420
- contextWindow: 200000,
421
- maxTokens: 65536
537
+ contextWindow: 1e6,
538
+ maxTokens: 64000
422
539
  }
423
540
  ];
424
541
  REASONING_FAMILIES = new Set([
@@ -863,6 +980,9 @@ var init_kiro_cli_sync = __esm(() => {
863
980
  init_debug();
864
981
  });
865
982
 
983
+ // src/server.ts
984
+ import { createHash as createHash4 } from "node:crypto";
985
+
866
986
  // src/types.ts
867
987
  class EventStream {
868
988
  queue = [];
@@ -870,20 +990,25 @@ class EventStream {
870
990
  done = false;
871
991
  finalResultPromise;
872
992
  resolveFinalResult;
993
+ rejectFinalResult;
994
+ resultSettled = false;
873
995
  isComplete;
874
996
  extractResult;
875
997
  constructor(isComplete, extractResult) {
876
998
  this.isComplete = isComplete;
877
999
  this.extractResult = extractResult;
878
- this.finalResultPromise = new Promise((resolve) => {
1000
+ this.finalResultPromise = new Promise((resolve, reject) => {
879
1001
  this.resolveFinalResult = resolve;
1002
+ this.rejectFinalResult = reject;
880
1003
  });
1004
+ this.finalResultPromise.catch(() => {});
881
1005
  }
882
1006
  push(event) {
883
1007
  if (this.done)
884
1008
  return;
885
1009
  if (this.isComplete(event)) {
886
1010
  this.done = true;
1011
+ this.resultSettled = true;
887
1012
  this.resolveFinalResult(this.extractResult(event));
888
1013
  }
889
1014
  const waiter = this.waiting.shift();
@@ -896,7 +1021,11 @@ class EventStream {
896
1021
  end(result) {
897
1022
  this.done = true;
898
1023
  if (result !== undefined) {
1024
+ this.resultSettled = true;
899
1025
  this.resolveFinalResult(result);
1026
+ } else if (!this.resultSettled) {
1027
+ this.resultSettled = true;
1028
+ this.rejectFinalResult(new Error("Stream ended before producing a final result"));
900
1029
  }
901
1030
  while (this.waiting.length > 0) {
902
1031
  const waiter = this.waiting.shift();
@@ -1086,6 +1215,9 @@ function parseKiroEventMulti(parsed) {
1086
1215
  if (parsed.unit === "credit" && parsed.usage !== undefined && typeof parsed.usage === "number") {
1087
1216
  events.push({ type: "metering", data: { usage: parsed.usage } });
1088
1217
  }
1218
+ if (typeof parsed.stopReason === "string") {
1219
+ events.push({ type: "metadata", data: { stopReason: parsed.stopReason } });
1220
+ }
1089
1221
  return events;
1090
1222
  }
1091
1223
  var EVENT_PATTERNS = [
@@ -1100,6 +1232,7 @@ var EVENT_PATTERNS = [
1100
1232
  '{"input":',
1101
1233
  '{"stop":',
1102
1234
  '{"contextUsagePercentage":',
1235
+ '{"stopReason":',
1103
1236
  '{"followupPrompt":',
1104
1237
  '{"usage":',
1105
1238
  '{"Usage":',
@@ -1118,9 +1251,31 @@ function findNextEventStart(buffer, from) {
1118
1251
  }
1119
1252
  return earliest;
1120
1253
  }
1254
+ var EXCEPTION_TYPE_RE = /:exception-type[\s\S]{0,4}?([A-Za-z][A-Za-z0-9]*(?:Exception|Error|Fault))/;
1255
+ var MESSAGE_TYPE_EXCEPTION_RE = /:message-type[\s\S]{0,4}?exception\b/;
1256
+ function detectEventStreamException(buffer) {
1257
+ const typeMatch = buffer.match(EXCEPTION_TYPE_RE);
1258
+ if (!typeMatch && !MESSAGE_TYPE_EXCEPTION_RE.test(buffer))
1259
+ return null;
1260
+ const type = typeMatch?.[1] ?? "ServiceException";
1261
+ let message;
1262
+ const msgIdx = buffer.lastIndexOf('{"message":');
1263
+ if (msgIdx >= 0) {
1264
+ const end = findJsonEnd(buffer, msgIdx);
1265
+ if (end >= 0) {
1266
+ try {
1267
+ const parsed = JSON.parse(buffer.substring(msgIdx, end + 1));
1268
+ if (typeof parsed.message === "string")
1269
+ message = parsed.message;
1270
+ } catch {}
1271
+ }
1272
+ }
1273
+ return { type, message };
1274
+ }
1121
1275
  function parseKiroEvents(buffer) {
1122
1276
  const events = [];
1123
1277
  let pos = 0;
1278
+ let remaining = "";
1124
1279
  while (pos < buffer.length) {
1125
1280
  const jsonStart = findNextEventStart(buffer, pos);
1126
1281
  if (jsonStart < 0) {
@@ -1148,7 +1303,8 @@ function parseKiroEvents(buffer) {
1148
1303
  }
1149
1304
  const jsonEnd = findJsonEnd(buffer, jsonStart);
1150
1305
  if (jsonEnd < 0) {
1151
- return { events, remaining: buffer.substring(jsonStart) };
1306
+ remaining = buffer.substring(jsonStart);
1307
+ break;
1152
1308
  }
1153
1309
  try {
1154
1310
  const parsed = JSON.parse(buffer.substring(jsonStart, jsonEnd + 1));
@@ -1168,7 +1324,12 @@ function parseKiroEvents(buffer) {
1168
1324
  }
1169
1325
  pos = jsonEnd + 1;
1170
1326
  }
1171
- return { events, remaining: "" };
1327
+ const exception = detectEventStreamException(buffer);
1328
+ if (exception && !events.some((e) => e.type === "error")) {
1329
+ events.push({ type: "error", data: { error: exception.type, message: exception.message } });
1330
+ remaining = "";
1331
+ }
1332
+ return { events, remaining };
1172
1333
  }
1173
1334
 
1174
1335
  // src/health.ts
@@ -1191,6 +1352,7 @@ function isPermanentError(reason) {
1191
1352
 
1192
1353
  // src/stream.ts
1193
1354
  init_models();
1355
+ import { createHash as createHash3 } from "node:crypto";
1194
1356
 
1195
1357
  // src/thinking-parser.ts
1196
1358
  init_debug();
@@ -1391,14 +1553,8 @@ class ThinkingTagParser {
1391
1553
  if (!thinking)
1392
1554
  return;
1393
1555
  if (this.thinkingBlockIndex === null) {
1394
- if (this.textBlockIndex !== null) {
1395
- this.thinkingBlockIndex = this.textBlockIndex;
1396
- this.output.content.splice(this.thinkingBlockIndex, 0, { type: "thinking", thinking: "" });
1397
- this.textBlockIndex = this.textBlockIndex + 1;
1398
- } else {
1399
- this.thinkingBlockIndex = this.output.content.length;
1400
- this.output.content.push({ type: "thinking", thinking: "" });
1401
- }
1556
+ this.thinkingBlockIndex = this.output.content.length;
1557
+ this.output.content.push({ type: "thinking", thinking: "" });
1402
1558
  this.stream.push({
1403
1559
  type: "thinking_start",
1404
1560
  contentIndex: this.thinkingBlockIndex,
@@ -2002,83 +2158,8 @@ async function startSocialLogin() {
2002
2158
  return { signInUrl, waitForCredentials };
2003
2159
  }
2004
2160
 
2005
- // src/file-logger.ts
2006
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
2007
- var LOG_DIR = "/tmp/kiro-logs";
2008
- var REQUESTS_FILE = `${LOG_DIR}/requests.log`;
2009
- var RESPONSES_FILE = `${LOG_DIR}/responses.log`;
2010
- var ERRORS_FILE = `${LOG_DIR}/errors.log`;
2011
- var dirEnsured = false;
2012
- function isEnabled() {
2013
- return true;
2014
- }
2015
- function ensureDir() {
2016
- if (dirEnsured)
2017
- return;
2018
- try {
2019
- mkdirSync2(LOG_DIR, { recursive: true });
2020
- dirEnsured = true;
2021
- } catch {}
2022
- }
2023
- function writeLine(file, data) {
2024
- if (!isEnabled())
2025
- return;
2026
- ensureDir();
2027
- const entry = {
2028
- ts: new Date().toISOString(),
2029
- ...data
2030
- };
2031
- try {
2032
- appendFileSync2(file, JSON.stringify(entry) + `
2033
- `);
2034
- } catch {}
2035
- }
2036
- function logRequest(meta, requestBody) {
2037
- writeLine(REQUESTS_FILE, {
2038
- type: "request",
2039
- ...meta,
2040
- body: safeParseJson(requestBody)
2041
- });
2042
- }
2043
- function logResponseEvent(event) {
2044
- writeLine(RESPONSES_FILE, {
2045
- type: "response_event",
2046
- eventType: event.type,
2047
- seq: event.eventSeq,
2048
- data: event.data
2049
- });
2050
- }
2051
- function logResponseDone(meta) {
2052
- writeLine(RESPONSES_FILE, {
2053
- type: "response_done",
2054
- ...meta
2055
- });
2056
- }
2057
- function logHttpError(meta) {
2058
- writeLine(ERRORS_FILE, {
2059
- type: "http_error",
2060
- ...meta
2061
- });
2062
- }
2063
- function logStreamError(meta) {
2064
- writeLine(ERRORS_FILE, {
2065
- type: "stream_error",
2066
- ...meta
2067
- });
2068
- }
2069
- function logCaughtError(meta) {
2070
- writeLine(ERRORS_FILE, {
2071
- type: "caught_error",
2072
- ...meta
2073
- });
2074
- }
2075
- function safeParseJson(s) {
2076
- try {
2077
- return JSON.parse(s);
2078
- } catch {
2079
- return s;
2080
- }
2081
- }
2161
+ // src/stream.ts
2162
+ init_file_logger();
2082
2163
 
2083
2164
  // src/transform.ts
2084
2165
  import { createHash as createHash2 } from "node:crypto";
@@ -2256,6 +2337,13 @@ function convertToolsToKiro(tools) {
2256
2337
  };
2257
2338
  });
2258
2339
  }
2340
+ var KIRO_IMAGE_FORMATS = new Set(["png", "jpeg", "gif", "webp"]);
2341
+ function normalizeImageFormat(mimeType) {
2342
+ const sub = (mimeType.split("/")[1] || "").toLowerCase().split(";")[0].trim();
2343
+ const base = sub.replace(/\+.*$/, "").replace(/^vnd\./, "");
2344
+ const canonical = base === "jpg" ? "jpeg" : base;
2345
+ return KIRO_IMAGE_FORMATS.has(canonical) ? canonical : null;
2346
+ }
2259
2347
  function convertImagesToKiro(images) {
2260
2348
  let omitted = 0;
2261
2349
  const valid = [];
@@ -2269,8 +2357,13 @@ function convertImagesToKiro(images) {
2269
2357
  omitted++;
2270
2358
  continue;
2271
2359
  }
2360
+ const format = normalizeImageFormat(img.mimeType);
2361
+ if (!format) {
2362
+ omitted++;
2363
+ continue;
2364
+ }
2272
2365
  valid.push({
2273
- format: img.mimeType.split("/")[1] || "png",
2366
+ format,
2274
2367
  source: { bytes: img.data }
2275
2368
  });
2276
2369
  }
@@ -2529,9 +2622,28 @@ function isTransientError(status) {
2529
2622
  return status === 429 || status >= 500;
2530
2623
  }
2531
2624
  function firstTokenTimeoutForModel(modelId) {
2532
- const m = kiroModels.find((x) => x.id === modelId);
2625
+ const m = kiroModels.find((x) => x.id === modelId) ?? getCachedDynamicModels()?.find((x) => x.id === modelId);
2533
2626
  return m?.firstTokenTimeout ?? FIRST_TOKEN_TIMEOUT_DEFAULT_MS;
2534
2627
  }
2628
+ function regionFromEndpoint(endpoint) {
2629
+ const m = endpoint.match(/(?:runtime|management)\.([a-z0-9-]+)\.kiro\.dev/i);
2630
+ return m?.[1] ?? "us-east-1";
2631
+ }
2632
+ function mapKiroStopReason(raw) {
2633
+ switch (raw?.toUpperCase()) {
2634
+ case "TOOL_USE":
2635
+ return "toolUse";
2636
+ case "MAX_TOKENS":
2637
+ return "length";
2638
+ case "END_TURN":
2639
+ case "STOP_SEQUENCE":
2640
+ case "COMPLETE":
2641
+ case "FINISHED":
2642
+ return "stop";
2643
+ default:
2644
+ return null;
2645
+ }
2646
+ }
2535
2647
  var HIDDEN_REASONING_PLACEHOLDER = "Reasoning hidden by provider";
2536
2648
  var HIDDEN_REASONING_COUNTDOWN_MS = 2000;
2537
2649
  function emitHiddenReasoningLate(output, stream) {
@@ -2577,8 +2689,23 @@ function emitToolCall(state, output, stream) {
2577
2689
  stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
2578
2690
  return true;
2579
2691
  }
2692
+ var CONVERSATION_ID_NAMESPACE = "opencode-kiro/conversation";
2693
+ function deterministicConversationId(key) {
2694
+ const digest = createHash3("sha1").update(`${CONVERSATION_ID_NAMESPACE}\x00${key}`).digest();
2695
+ const b = Buffer.from(digest.subarray(0, 16));
2696
+ b[6] = b[6] & 15 | 80;
2697
+ b[8] = b[8] & 63 | 128;
2698
+ const hex = b.toString("hex");
2699
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
2700
+ }
2701
+ function resolveConversationId(sessionId) {
2702
+ if (!sessionId)
2703
+ return crypto.randomUUID();
2704
+ return deterministicConversationId(sessionId);
2705
+ }
2580
2706
  function streamKiro(model, context, options) {
2581
2707
  const stream = new AssistantMessageEventStream;
2708
+ const fileLog = createSessionLogger(options?.logSessionId ?? options?.sessionId);
2582
2709
  (async () => {
2583
2710
  const output = {
2584
2711
  role: "assistant",
@@ -2604,7 +2731,7 @@ function streamKiro(model, context, options) {
2604
2731
  throw new Error("Kiro credentials not set. Run /login kiro.");
2605
2732
  }
2606
2733
  const endpoint = model.baseUrl || "https://runtime.us-east-1.kiro.dev";
2607
- const profileArn = await resolveProfileArn(accessToken, endpoint);
2734
+ const profileArn = await resolveProfileArn(accessToken, regionFromEndpoint(endpoint));
2608
2735
  const kiroModelId = resolveKiroModel(model.id);
2609
2736
  const thinkingEnabled = !!options?.reasoning || model.reasoning;
2610
2737
  const reasoningHidden = !!model.reasoningHidden;
@@ -2633,7 +2760,7 @@ ${systemPrompt}` : ""}`;
2633
2760
  operatingSystem: resolveOS(),
2634
2761
  currentWorkingDirectory: process.cwd()
2635
2762
  };
2636
- const conversationId = options?.sessionId ?? crypto.randomUUID();
2763
+ const conversationId = resolveConversationId(options?.sessionId);
2637
2764
  let retryCount = 0;
2638
2765
  while (retryCount <= MAX_RETRIES) {
2639
2766
  if (options?.signal?.aborted)
@@ -2828,6 +2955,12 @@ ${currentContent}`;
2828
2955
  log.debug("effort.set", { effort: options.reasoning, model: model.id });
2829
2956
  }
2830
2957
  }
2958
+ if (supportsThinkingConfig && typeof options?.maxTokens === "number" && options.maxTokens > 0) {
2959
+ const capped = Math.min(Math.max(Math.floor(options.maxTokens), 1024), model.maxTokens || 64000);
2960
+ request.additionalModelRequestFields = request.additionalModelRequestFields || {};
2961
+ request.additionalModelRequestFields.max_tokens = capped;
2962
+ log.debug("maxTokens.set", { maxTokens: capped, model: model.id });
2963
+ }
2831
2964
  stream.push({ type: "start", partial: output });
2832
2965
  if (reasoningHidden && thinkingEnabled && hiddenShimTimer === null) {
2833
2966
  hiddenShimTimer = setTimeout(() => {
@@ -2841,7 +2974,7 @@ ${currentContent}`;
2841
2974
  let contextTruncationAttempt = 0;
2842
2975
  while (true) {
2843
2976
  const osName = resolveOS();
2844
- const ua = `aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererstreaming/0.1.16551 os/${osName} lang/rust/1.92.0 md/appVersion-2.7.1 app/AmazonQ-For-CLI`;
2977
+ const ua = `aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererstreaming/0.1.16551 os/${osName} lang/rust/1.92.0 md/appVersion-2.8.1 app/AmazonQ-For-CLI`;
2845
2978
  const xAmzUa = `aws-sdk-rust/1.3.15 ua/2.1 api/codewhispererstreaming/0.1.16551 os/${osName} lang/rust/1.92.0 m/F app/AmazonQ-For-CLI`;
2846
2979
  const requestBody = JSON.stringify(request);
2847
2980
  log.debug("request.send", {
@@ -2856,10 +2989,11 @@ ${currentContent}`;
2856
2989
  log.debug(`[stream] req=${requestBody.length}c hist=${history.length} content=${currentContent.length}c profileArn=${!!profileArn}`);
2857
2990
  if (log.isDebug()) {
2858
2991
  try {
2859
- __require("fs").writeFileSync("/tmp/kiro-last-request.json", requestBody);
2992
+ ensureLogDir();
2993
+ __require("fs").writeFileSync(`${LOG_DIR}/session-${fileLog.sessionId}.last-request.json`, requestBody);
2860
2994
  } catch {}
2861
2995
  }
2862
- logRequest({
2996
+ fileLog.logRequest({
2863
2997
  endpoint,
2864
2998
  model: model.id,
2865
2999
  historyLength: history.length,
@@ -2898,7 +3032,7 @@ ${currentContent}`;
2898
3032
  status: response.status,
2899
3033
  body: errText
2900
3034
  });
2901
- logHttpError({
3035
+ fileLog.logHttpError({
2902
3036
  status: response.status,
2903
3037
  statusText: response.statusText,
2904
3038
  body: errText,
@@ -2965,10 +3099,10 @@ ${currentContent}`;
2965
3099
  const decoder = new TextDecoder;
2966
3100
  let buffer = "";
2967
3101
  let totalContent = "";
2968
- let lastContentData = "";
2969
3102
  let usageEvent = null;
2970
3103
  let meteringCredits;
2971
3104
  let receivedContextUsage = false;
3105
+ let serverStopReason = null;
2972
3106
  let chunkSeq = 0;
2973
3107
  let eventSeq = 0;
2974
3108
  const thinkingParser = thinkingEnabled ? new ThinkingTagParser(output, stream) : null;
@@ -3051,7 +3185,7 @@ ${currentContent}`;
3051
3185
  }
3052
3186
  }
3053
3187
  for (const ev of events) {
3054
- logResponseEvent({ type: ev.type, data: ev.data, eventSeq });
3188
+ fileLog.logResponseEvent({ type: ev.type, data: ev.data, eventSeq });
3055
3189
  }
3056
3190
  for (const event of events) {
3057
3191
  switch (event.type) {
@@ -3064,7 +3198,10 @@ ${currentContent}`;
3064
3198
  }
3065
3199
  case "reasoning": {
3066
3200
  cancelHiddenShim();
3067
- if (output.content.length === 0 || output.content[output.content.length - 1]?.type !== "thinking") {
3201
+ const lastIsThinking = output.content.length > 0 && output.content[output.content.length - 1]?.type === "thinking";
3202
+ if (!event.data.text && !lastIsThinking)
3203
+ break;
3204
+ if (!lastIsThinking) {
3068
3205
  output.content.push({ type: "thinking", thinking: "" });
3069
3206
  stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output });
3070
3207
  }
@@ -3081,9 +3218,6 @@ ${currentContent}`;
3081
3218
  break;
3082
3219
  }
3083
3220
  case "content": {
3084
- if (event.data === lastContentData)
3085
- continue;
3086
- lastContentData = event.data;
3087
3221
  totalContent += event.data;
3088
3222
  cancelHiddenShim();
3089
3223
  if (thinkingParser) {
@@ -3142,9 +3276,14 @@ ${currentContent}`;
3142
3276
  meteringCredits = event.data.usage;
3143
3277
  break;
3144
3278
  }
3279
+ case "metadata": {
3280
+ if (event.data.stopReason)
3281
+ serverStopReason = event.data.stopReason;
3282
+ break;
3283
+ }
3145
3284
  case "error": {
3146
3285
  streamError = event.data.message ? `${event.data.error}: ${event.data.message}` : event.data.error;
3147
- logStreamError({
3286
+ fileLog.logStreamError({
3148
3287
  error: streamError,
3149
3288
  context: "stream_event",
3150
3289
  model: model.id,
@@ -3161,11 +3300,12 @@ ${currentContent}`;
3161
3300
  if (idleTimer)
3162
3301
  clearTimeout(idleTimer);
3163
3302
  if (firstTokenTimedOut || idleCancelled || streamError) {
3164
- if (retryCount < MAX_RETRIES) {
3303
+ const alreadyStreamed = totalContent.length > 0 || emittedToolCalls > 0;
3304
+ if (!alreadyStreamed && retryCount < MAX_RETRIES) {
3165
3305
  retryCount++;
3166
3306
  const delayMs = exponentialBackoff(retryCount - 1, 1000, MAX_RETRY_DELAY_MS);
3167
3307
  const streamErrDesc = firstTokenTimedOut ? "first-token timed out" : idleCancelled ? "idle timed out" : `error: ${streamError}`;
3168
- logStreamError({
3308
+ fileLog.logStreamError({
3169
3309
  error: streamErrDesc,
3170
3310
  context: "retry",
3171
3311
  model: model.id,
@@ -3178,9 +3318,13 @@ ${currentContent}`;
3178
3318
  textBlockIndex = null;
3179
3319
  continue;
3180
3320
  }
3181
- if (streamError)
3182
- throw new Error(`Kiro API stream error after max retries: ${streamError}`);
3183
- throw new Error(`Kiro API error: ${firstTokenTimedOut ? "first token" : "idle"} timeout after max retries`);
3321
+ if (streamError) {
3322
+ throw new Error(`Kiro API stream error${alreadyStreamed ? " after partial output" : " after max retries"}: ${streamError}`);
3323
+ }
3324
+ if (!alreadyStreamed) {
3325
+ throw new Error(`Kiro API error: ${firstTokenTimedOut ? "first token" : "idle"} timeout after max retries`);
3326
+ }
3327
+ log.info(`stream ${firstTokenTimedOut ? "first-token" : "idle"} timeout after partial output — finalizing with partial content`);
3184
3328
  }
3185
3329
  cancelHiddenShim();
3186
3330
  if (currentToolCall && emitToolCall(currentToolCall, output, stream))
@@ -3230,7 +3374,10 @@ ${currentContent}`;
3230
3374
  log.info(`empty response persisted after ${MAX_RETRIES} retries`);
3231
3375
  cancelHiddenShim();
3232
3376
  }
3233
- if (!receivedContextUsage && emittedToolCalls === 0) {
3377
+ const mappedServerStop = mapKiroStopReason(serverStopReason);
3378
+ if (mappedServerStop) {
3379
+ output.stopReason = mappedServerStop;
3380
+ } else if (!receivedContextUsage && emittedToolCalls === 0) {
3234
3381
  output.stopReason = "length";
3235
3382
  } else {
3236
3383
  output.stopReason = emittedToolCalls > 0 ? "toolUse" : "stop";
@@ -3246,7 +3393,7 @@ ${currentContent}`;
3246
3393
  sawAnyToolCalls,
3247
3394
  usage: output.usage
3248
3395
  });
3249
- logResponseDone({
3396
+ fileLog.logResponseDone({
3250
3397
  stopReason: output.stopReason,
3251
3398
  emittedToolCalls,
3252
3399
  usage: output.usage,
@@ -3260,11 +3407,11 @@ ${currentContent}`;
3260
3407
  output.stopReason = options?.signal?.aborted ? "aborted" : "error";
3261
3408
  output.errorMessage = error instanceof Error ? error.message : String(error);
3262
3409
  log.debug("response.caught", { stopReason: output.stopReason, error: output.errorMessage });
3263
- logCaughtError({
3410
+ fileLog.logCaughtError({
3264
3411
  stopReason: output.stopReason,
3265
3412
  errorMessage: output.errorMessage,
3266
3413
  model: model.id
3267
- });
3414
+ }, error);
3268
3415
  if (hiddenShimTimer) {
3269
3416
  clearTimeout(hiddenShimTimer);
3270
3417
  hiddenShimTimer = null;
@@ -3282,6 +3429,7 @@ ${currentContent}`;
3282
3429
 
3283
3430
  // src/server.ts
3284
3431
  init_debug();
3432
+ init_file_logger();
3285
3433
  init_models();
3286
3434
  init_models();
3287
3435
 
@@ -3715,6 +3863,7 @@ function getDashboardHtml() {
3715
3863
 
3716
3864
  // src/server.ts
3717
3865
  var _creds = null;
3866
+ var _refreshInFlight = null;
3718
3867
  async function initGatewayAuth() {
3719
3868
  try {
3720
3869
  const { importFromKiroCli: importFromKiroCli2 } = await Promise.resolve().then(() => (init_kiro_cli_sync(), exports_kiro_cli_sync));
@@ -3727,7 +3876,9 @@ async function initGatewayAuth() {
3727
3876
  imported.refreshToken,
3728
3877
  imported.clientId || "",
3729
3878
  imported.clientSecret || "",
3730
- imported.authMethod
3879
+ imported.authMethod,
3880
+ imported.source || "",
3881
+ imported.tokenKey || ""
3731
3882
  ];
3732
3883
  _creds = {
3733
3884
  accessToken: imported.accessToken,
@@ -3772,15 +3923,37 @@ async function getAccessToken() {
3772
3923
  if (!_creds)
3773
3924
  throw new Error("Kiro credentials not initialized — run /login kiro");
3774
3925
  if (Date.now() >= _creds.expiresAt) {
3775
- log.info("[gateway-auth] Token expired, refreshing...");
3776
- const refreshed = await refreshKiroToken(_creds.refreshPacked, _creds.region, _creds.authMethod);
3777
- _creds.accessToken = refreshed.access;
3778
- _creds.refreshPacked = refreshed.refresh;
3779
- _creds.expiresAt = refreshed.expires;
3780
- log.info("[gateway-auth] Token refreshed successfully");
3926
+ if (!_refreshInFlight) {
3927
+ const creds = _creds;
3928
+ _refreshInFlight = (async () => {
3929
+ log.info("[gateway-auth] Token expired, refreshing...");
3930
+ const refreshed = await refreshKiroToken(creds.refreshPacked, creds.region, creds.authMethod);
3931
+ creds.accessToken = refreshed.access;
3932
+ creds.refreshPacked = refreshed.refresh;
3933
+ creds.expiresAt = refreshed.expires;
3934
+ log.info("[gateway-auth] Token refreshed successfully");
3935
+ })().finally(() => {
3936
+ _refreshInFlight = null;
3937
+ });
3938
+ }
3939
+ await _refreshInFlight;
3781
3940
  }
3782
3941
  return _creds.accessToken;
3783
3942
  }
3943
+ function isLocalhostOrigin(origin) {
3944
+ try {
3945
+ const host = new URL(origin).hostname;
3946
+ return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
3947
+ } catch {
3948
+ return false;
3949
+ }
3950
+ }
3951
+ function isDisallowedBrowserRequest(req) {
3952
+ const origin = req.headers.get("origin");
3953
+ if (!origin)
3954
+ return false;
3955
+ return !isLocalhostOrigin(origin);
3956
+ }
3784
3957
  function anthropicError(status, type, message) {
3785
3958
  return new Response(JSON.stringify({ type: "error", error: { type, message } }), {
3786
3959
  status,
@@ -3790,6 +3963,82 @@ function anthropicError(status, type, message) {
3790
3963
  }
3791
3964
  });
3792
3965
  }
3966
+ function isTitleGenerationRequest(messages) {
3967
+ for (const m of messages) {
3968
+ if (m?.role !== "user")
3969
+ continue;
3970
+ const text = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((b) => typeof b === "string" ? b : b?.text || "").join(" ") : "";
3971
+ if (/generate a title for this conversation/i.test(text))
3972
+ return true;
3973
+ }
3974
+ return false;
3975
+ }
3976
+ function shortHash(input) {
3977
+ return createHash4("sha256").update(input).digest("hex").slice(0, 12);
3978
+ }
3979
+ function firstUserMessageText(messages) {
3980
+ for (const m of messages) {
3981
+ if (m?.role !== "user")
3982
+ continue;
3983
+ if (typeof m.content === "string")
3984
+ return m.content;
3985
+ if (Array.isArray(m.content)) {
3986
+ const text = m.content.map((b) => typeof b === "string" ? b : b?.text || "").join(" ").trim();
3987
+ if (text)
3988
+ return text;
3989
+ }
3990
+ }
3991
+ return "";
3992
+ }
3993
+ function conversationSeed(messages) {
3994
+ const text = firstUserMessageText(messages);
3995
+ if (text)
3996
+ return text;
3997
+ if (messages.length > 0) {
3998
+ try {
3999
+ return "msg0:" + JSON.stringify(messages[0]);
4000
+ } catch {}
4001
+ }
4002
+ return "";
4003
+ }
4004
+ function deriveLogSessionId(body, messages, headers) {
4005
+ const headerId = headers?.get("x-session-id") || headers?.get("x-kiro-session-id") || headers?.get("anthropic-session-id");
4006
+ if (headerId && headerId.trim().length > 0) {
4007
+ return `s-${shortHash(headerId.trim())}`;
4008
+ }
4009
+ const userId = body?.metadata?.user_id;
4010
+ if (typeof userId === "string" && userId.trim().length > 0) {
4011
+ return `u-${shortHash(userId.trim())}`;
4012
+ }
4013
+ const seed = conversationSeed(messages);
4014
+ if (isTitleGenerationRequest(messages)) {
4015
+ return `title-${shortHash(seed || "untitled")}`;
4016
+ }
4017
+ if (seed) {
4018
+ return `c-${shortHash(seed)}`;
4019
+ }
4020
+ try {
4021
+ return `c-${shortHash(JSON.stringify(body))}`;
4022
+ } catch {
4023
+ return `c-${shortHash(String(Date.now()))}`;
4024
+ }
4025
+ }
4026
+ function stripTitleMarkdown(text) {
4027
+ let t = text.trim();
4028
+ let prev;
4029
+ do {
4030
+ prev = t;
4031
+ t = t.replace(/^\*\*([\s\S]+?)\*\*$/, "$1").trim();
4032
+ t = t.replace(/^\*([\s\S]+?)\*$/, "$1").trim();
4033
+ t = t.replace(/^__([\s\S]+?)__$/, "$1").trim();
4034
+ t = t.replace(/^_([\s\S]+?)_$/, "$1").trim();
4035
+ t = t.replace(/^`([\s\S]+?)`$/, "$1").trim();
4036
+ t = t.replace(/^["'“”]([\s\S]+?)["'“”]$/, "$1").trim();
4037
+ t = t.replace(/^#{1,6}\s+/, "").trim();
4038
+ t = t.replace(/^[-*]\s+/, "").trim();
4039
+ } while (t !== prev && t.length > 0);
4040
+ return t;
4041
+ }
3793
4042
  function startGatewayServer(port = 0) {
3794
4043
  return new Promise((resolve2) => {
3795
4044
  const server = Bun.serve({
@@ -3797,13 +4046,16 @@ function startGatewayServer(port = 0) {
3797
4046
  idleTimeout: 255,
3798
4047
  async fetch(req) {
3799
4048
  if (req.method === "OPTIONS") {
3800
- return new Response(null, {
3801
- headers: {
3802
- "Access-Control-Allow-Origin": "*",
3803
- "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
3804
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
3805
- }
3806
- });
4049
+ const origin = req.headers.get("origin");
4050
+ const headers = {
4051
+ "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
4052
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
4053
+ };
4054
+ if (origin && isLocalhostOrigin(origin)) {
4055
+ headers["Access-Control-Allow-Origin"] = origin;
4056
+ headers["Vary"] = "Origin";
4057
+ }
4058
+ return new Response(null, { headers });
3807
4059
  }
3808
4060
  const url = new URL(req.url);
3809
4061
  if (url.pathname === "/dashboard") {
@@ -3852,6 +4104,9 @@ function startGatewayServer(port = 0) {
3852
4104
  }
3853
4105
  }
3854
4106
  if ((url.pathname === "/v1/messages" || url.pathname === "/messages") && req.method === "POST") {
4107
+ if (isDisallowedBrowserRequest(req)) {
4108
+ return anthropicError(403, "invalid_request_error", "Cross-origin requests are not allowed");
4109
+ }
3855
4110
  let accessToken;
3856
4111
  try {
3857
4112
  accessToken = await getAccessToken();
@@ -3876,6 +4131,9 @@ function startGatewayServer(port = 0) {
3876
4131
  }
3877
4132
  const streamRequested = !!body.stream;
3878
4133
  const temperature = body.temperature ?? 0.5;
4134
+ const maxTokens = typeof body.max_tokens === "number" ? body.max_tokens : undefined;
4135
+ const logSessionId = deriveLogSessionId(body, anthropicMessages, req.headers);
4136
+ enterSessionLog(logSessionId);
3879
4137
  log.debug(`[gateway] sys=${systemPrompt.length}c msgs=${anthropicMessages.length} tools=${body.tools?.length ?? 0}`);
3880
4138
  try {
3881
4139
  const piMessages = translateAnthropicToPi(anthropicMessages);
@@ -3900,6 +4158,7 @@ function startGatewayServer(port = 0) {
3900
4158
  };
3901
4159
  log.debug("[gateway] body-keys", Object.keys(body));
3902
4160
  const reasoningEffort = body.output_config?.effort ?? body.reasoning_effort ?? undefined;
4161
+ const isTitleTurn = isTitleGenerationRequest(anthropicMessages);
3903
4162
  log.info(`[gateway] → ${kiroEndpoint} model=${anthropicModelId} region=${apiRegion} stream=${streamRequested}`);
3904
4163
  if (_creds.profileArn) {
3905
4164
  seedProfileArn(_creds.profileArn);
@@ -3907,7 +4166,10 @@ function startGatewayServer(port = 0) {
3907
4166
  const kiroStream = streamKiro(piModel, context, {
3908
4167
  apiKey: accessToken,
3909
4168
  reasoning: reasoningEffort,
3910
- temperature
4169
+ temperature,
4170
+ maxTokens,
4171
+ sessionId: logSessionId,
4172
+ logSessionId
3911
4173
  });
3912
4174
  if (streamRequested) {
3913
4175
  const iter = kiroStream[Symbol.asyncIterator]();
@@ -3943,6 +4205,29 @@ function startGatewayServer(port = 0) {
3943
4205
  }
3944
4206
  const streamResponse = new ReadableStream({
3945
4207
  async start(controller) {
4208
+ const PING_INTERVAL_MS = 15000;
4209
+ let lastActivity = Date.now();
4210
+ let pingTimer = null;
4211
+ const stopHeartbeat = () => {
4212
+ if (pingTimer) {
4213
+ clearInterval(pingTimer);
4214
+ pingTimer = null;
4215
+ }
4216
+ };
4217
+ const startHeartbeat = () => {
4218
+ pingTimer = setInterval(() => {
4219
+ if (Date.now() - lastActivity < PING_INTERVAL_MS)
4220
+ return;
4221
+ try {
4222
+ controller.enqueue(`event: ping
4223
+ data: {"type":"ping"}
4224
+
4225
+ `);
4226
+ } catch {
4227
+ stopHeartbeat();
4228
+ }
4229
+ }, PING_INTERVAL_MS);
4230
+ };
3946
4231
  try {
3947
4232
  const msgId = `msg_${crypto.randomUUID()}`;
3948
4233
  controller.enqueue(`event: message_start
@@ -3961,8 +4246,10 @@ data: ` + JSON.stringify({
3961
4246
  }) + `
3962
4247
 
3963
4248
  `);
4249
+ startHeartbeat();
3964
4250
  let contentBlockIndex = 0;
3965
4251
  let activeBlockType = null;
4252
+ let titleTextBuffer = "";
3966
4253
  const closeActiveBlock = () => {
3967
4254
  if (activeBlockType !== null) {
3968
4255
  controller.enqueue(`event: content_block_stop
@@ -3993,6 +4280,7 @@ data: ` + JSON.stringify({
3993
4280
  `);
3994
4281
  };
3995
4282
  const processEvent = (event) => {
4283
+ lastActivity = Date.now();
3996
4284
  if (event.type === "thinking_delta") {
3997
4285
  ensureBlockStarted("thinking");
3998
4286
  controller.enqueue(`event: content_block_delta
@@ -4008,6 +4296,10 @@ data: ` + JSON.stringify({
4008
4296
  `);
4009
4297
  } else if (event.type === "text_delta") {
4010
4298
  ensureBlockStarted("text");
4299
+ if (isTitleTurn) {
4300
+ titleTextBuffer += event.delta;
4301
+ return;
4302
+ }
4011
4303
  controller.enqueue(`event: content_block_delta
4012
4304
  data: ` + JSON.stringify({
4013
4305
  type: "content_block_delta",
@@ -4058,9 +4350,34 @@ data: ` + JSON.stringify({
4058
4350
  for await (const event of { [Symbol.asyncIterator]: () => iter }) {
4059
4351
  processEvent(event);
4060
4352
  }
4353
+ if (isTitleTurn && titleTextBuffer.length > 0) {
4354
+ const cleanTitle = stripTitleMarkdown(titleTextBuffer);
4355
+ controller.enqueue(`event: content_block_delta
4356
+ data: ` + JSON.stringify({
4357
+ type: "content_block_delta",
4358
+ index: contentBlockIndex - 1,
4359
+ delta: { type: "text_delta", text: cleanTitle }
4360
+ }) + `
4361
+
4362
+ `);
4363
+ }
4061
4364
  closeActiveBlock();
4062
- let finishReason = "end_turn";
4063
4365
  const finalMsg = await kiroStream.result();
4366
+ if (finalMsg.stopReason === "error" || finalMsg.errorMessage) {
4367
+ controller.enqueue(`event: error
4368
+ data: ` + JSON.stringify({
4369
+ type: "error",
4370
+ error: {
4371
+ type: "api_error",
4372
+ message: finalMsg.errorMessage || "Kiro stream error"
4373
+ }
4374
+ }) + `
4375
+
4376
+ `);
4377
+ controller.close();
4378
+ return;
4379
+ }
4380
+ let finishReason = "end_turn";
4064
4381
  if (finalMsg.content.some((b) => b.type === "toolCall")) {
4065
4382
  finishReason = "tool_use";
4066
4383
  }
@@ -4108,6 +4425,8 @@ data: ` + JSON.stringify({
4108
4425
 
4109
4426
  `);
4110
4427
  controller.close();
4428
+ } finally {
4429
+ stopHeartbeat();
4111
4430
  }
4112
4431
  }
4113
4432
  });
@@ -4121,13 +4440,16 @@ data: ` + JSON.stringify({
4121
4440
  });
4122
4441
  } else {
4123
4442
  const finalMsg = await kiroStream.result();
4443
+ if (finalMsg.stopReason === "error" || finalMsg.errorMessage) {
4444
+ return anthropicError(502, "api_error", `Kiro: ${finalMsg.errorMessage || "stream error"}`);
4445
+ }
4124
4446
  const contentParts = finalMsg.content;
4125
4447
  const anthropicContent = [];
4126
4448
  for (const part of contentParts) {
4127
4449
  if (part.type === "text") {
4128
4450
  anthropicContent.push({
4129
4451
  type: "text",
4130
- text: part.text
4452
+ text: isTitleTurn ? stripTitleMarkdown(part.text) : part.text
4131
4453
  });
4132
4454
  } else if (part.type === "thinking") {
4133
4455
  anthropicContent.push({
@@ -4204,9 +4526,20 @@ function translateAnthropicToPi(messages) {
4204
4526
  timestamp: Date.now()
4205
4527
  });
4206
4528
  } else if (Array.isArray(msg.content)) {
4207
- const toolResultParts = msg.content.filter((part) => part.type === "tool_result");
4208
- if (toolResultParts.length > 0) {
4209
- for (const part of toolResultParts) {
4529
+ let pendingUserParts = [];
4530
+ const flushUserParts = () => {
4531
+ if (pendingUserParts.length > 0) {
4532
+ piMessages.push({
4533
+ role: "user",
4534
+ content: pendingUserParts,
4535
+ timestamp: Date.now()
4536
+ });
4537
+ pendingUserParts = [];
4538
+ }
4539
+ };
4540
+ for (const part of msg.content) {
4541
+ if (part.type === "tool_result") {
4542
+ flushUserParts();
4210
4543
  piMessages.push({
4211
4544
  role: "toolResult",
4212
4545
  toolCallId: part.tool_use_id,
@@ -4215,24 +4548,13 @@ function translateAnthropicToPi(messages) {
4215
4548
  isError: part.is_error || false,
4216
4549
  timestamp: Date.now()
4217
4550
  });
4551
+ } else if (part.type === "text") {
4552
+ pendingUserParts.push({ type: "text", text: part.text });
4553
+ } else if (part.type === "image" && part.source?.type === "base64") {
4554
+ pendingUserParts.push({ type: "image", mimeType: part.source.media_type, data: part.source.data });
4218
4555
  }
4219
4556
  }
4220
- const otherParts = msg.content.map((part) => {
4221
- if (part.type === "text") {
4222
- return { type: "text", text: part.text };
4223
- }
4224
- if (part.type === "image" && part.source?.type === "base64") {
4225
- return { type: "image", mimeType: part.source.media_type, data: part.source.data };
4226
- }
4227
- return null;
4228
- }).filter(Boolean);
4229
- if (otherParts.length > 0) {
4230
- piMessages.push({
4231
- role: "user",
4232
- content: otherParts,
4233
- timestamp: Date.now()
4234
- });
4235
- }
4557
+ flushUserParts();
4236
4558
  }
4237
4559
  } else if (msg.role === "assistant") {
4238
4560
  const contentParts = [];
@@ -4278,7 +4600,7 @@ function translateAnthropicToolsToPi(tools) {
4278
4600
  init_debug();
4279
4601
  init_models();
4280
4602
  process.env.KIRO_LOG = process.env.KIRO_LOG || "debug";
4281
- process.env.KIRO_LOG_FILE = process.env.KIRO_LOG_FILE || "/tmp/opencode-kiro.log";
4603
+ process.env.KIRO_LOG_FILE = process.env.KIRO_LOG_FILE || "/tmp/kiro-logs/session-gateway.log";
4282
4604
  var gatewayServer = null;
4283
4605
  var KiroPlugin = async (input) => {
4284
4606
  const client = input.client;