bopodev-agent-sdk 0.1.12 → 0.1.13

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 (100) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/dist/adapters/anthropic-api/src/cli/format-event.d.ts +1 -0
  4. package/dist/adapters/anthropic-api/src/cli/index.d.ts +1 -0
  5. package/dist/adapters/anthropic-api/src/index.d.ts +16 -0
  6. package/dist/adapters/anthropic-api/src/server/execute.d.ts +2 -0
  7. package/dist/adapters/anthropic-api/src/server/index.d.ts +6 -0
  8. package/dist/adapters/anthropic-api/src/server/parse.d.ts +1 -0
  9. package/dist/adapters/anthropic-api/src/server/test.d.ts +2 -0
  10. package/dist/adapters/anthropic-api/src/ui/build-config.d.ts +3 -0
  11. package/dist/adapters/anthropic-api/src/ui/index.d.ts +2 -0
  12. package/dist/adapters/anthropic-api/src/ui/parse-stdout.d.ts +6 -0
  13. package/dist/adapters/claude-code/src/cli/format-event.d.ts +1 -0
  14. package/dist/adapters/claude-code/src/cli/index.d.ts +1 -0
  15. package/dist/adapters/claude-code/src/index.d.ts +16 -0
  16. package/dist/adapters/claude-code/src/server/execute.d.ts +2 -0
  17. package/dist/adapters/claude-code/src/server/index.d.ts +6 -0
  18. package/dist/adapters/claude-code/src/server/parse.d.ts +2 -0
  19. package/dist/adapters/claude-code/src/server/test.d.ts +2 -0
  20. package/dist/adapters/claude-code/src/ui/build-config.d.ts +3 -0
  21. package/dist/adapters/claude-code/src/ui/index.d.ts +2 -0
  22. package/dist/adapters/claude-code/src/ui/parse-stdout.d.ts +6 -0
  23. package/dist/adapters/codex/src/cli/format-event.d.ts +1 -0
  24. package/dist/adapters/codex/src/cli/index.d.ts +1 -0
  25. package/dist/adapters/codex/src/index.d.ts +34 -0
  26. package/dist/adapters/codex/src/server/execute.d.ts +2 -0
  27. package/dist/adapters/codex/src/server/index.d.ts +6 -0
  28. package/dist/adapters/codex/src/server/parse.d.ts +2 -0
  29. package/dist/adapters/codex/src/server/test.d.ts +2 -0
  30. package/dist/adapters/codex/src/ui/build-config.d.ts +3 -0
  31. package/dist/adapters/codex/src/ui/index.d.ts +2 -0
  32. package/dist/adapters/codex/src/ui/parse-stdout.d.ts +6 -0
  33. package/dist/adapters/cursor/src/cli/format-event.d.ts +1 -0
  34. package/dist/adapters/cursor/src/cli/index.d.ts +1 -0
  35. package/dist/adapters/cursor/src/index.d.ts +22 -0
  36. package/dist/adapters/cursor/src/server/execute.d.ts +2 -0
  37. package/dist/adapters/cursor/src/server/index.d.ts +6 -0
  38. package/dist/adapters/cursor/src/server/parse.d.ts +2 -0
  39. package/dist/adapters/cursor/src/server/test.d.ts +2 -0
  40. package/dist/adapters/cursor/src/ui/build-config.d.ts +3 -0
  41. package/dist/adapters/cursor/src/ui/index.d.ts +2 -0
  42. package/dist/adapters/cursor/src/ui/parse-stdout.d.ts +6 -0
  43. package/dist/adapters/http/src/cli/format-event.d.ts +1 -0
  44. package/dist/adapters/http/src/cli/index.d.ts +1 -0
  45. package/dist/adapters/http/src/index.d.ts +7 -0
  46. package/dist/adapters/http/src/server/execute.d.ts +2 -0
  47. package/dist/adapters/http/src/server/index.d.ts +6 -0
  48. package/dist/adapters/http/src/server/parse.d.ts +1 -0
  49. package/dist/adapters/http/src/server/test.d.ts +2 -0
  50. package/dist/adapters/http/src/ui/build-config.d.ts +3 -0
  51. package/dist/adapters/http/src/ui/index.d.ts +2 -0
  52. package/dist/adapters/http/src/ui/parse-stdout.d.ts +6 -0
  53. package/dist/adapters/openai-api/src/cli/format-event.d.ts +1 -0
  54. package/dist/adapters/openai-api/src/cli/index.d.ts +1 -0
  55. package/dist/adapters/openai-api/src/index.d.ts +22 -0
  56. package/dist/adapters/openai-api/src/server/execute.d.ts +2 -0
  57. package/dist/adapters/openai-api/src/server/index.d.ts +6 -0
  58. package/dist/adapters/openai-api/src/server/parse.d.ts +1 -0
  59. package/dist/adapters/openai-api/src/server/test.d.ts +2 -0
  60. package/dist/adapters/openai-api/src/ui/build-config.d.ts +3 -0
  61. package/dist/adapters/openai-api/src/ui/index.d.ts +2 -0
  62. package/dist/adapters/openai-api/src/ui/parse-stdout.d.ts +6 -0
  63. package/dist/adapters/opencode/src/cli/format-event.d.ts +1 -0
  64. package/dist/adapters/opencode/src/cli/index.d.ts +1 -0
  65. package/dist/adapters/opencode/src/index.d.ts +7 -0
  66. package/dist/adapters/opencode/src/server/execute.d.ts +2 -0
  67. package/dist/adapters/opencode/src/server/index.d.ts +6 -0
  68. package/dist/adapters/opencode/src/server/parse.d.ts +1 -0
  69. package/dist/adapters/opencode/src/server/test.d.ts +2 -0
  70. package/dist/adapters/opencode/src/ui/build-config.d.ts +3 -0
  71. package/dist/adapters/opencode/src/ui/index.d.ts +2 -0
  72. package/dist/adapters/opencode/src/ui/parse-stdout.d.ts +6 -0
  73. package/dist/adapters/shell/src/cli/format-event.d.ts +1 -0
  74. package/dist/adapters/shell/src/cli/index.d.ts +1 -0
  75. package/dist/adapters/shell/src/index.d.ts +7 -0
  76. package/dist/adapters/shell/src/server/execute.d.ts +2 -0
  77. package/dist/adapters/shell/src/server/index.d.ts +6 -0
  78. package/dist/adapters/shell/src/server/parse.d.ts +1 -0
  79. package/dist/adapters/shell/src/server/test.d.ts +2 -0
  80. package/dist/adapters/shell/src/ui/build-config.d.ts +3 -0
  81. package/dist/adapters/shell/src/ui/index.d.ts +2 -0
  82. package/dist/adapters/shell/src/ui/parse-stdout.d.ts +6 -0
  83. package/dist/agent-sdk/src/adapters.d.ts +226 -1
  84. package/dist/agent-sdk/src/index.d.ts +2 -0
  85. package/dist/agent-sdk/src/registry.d.ts +2 -1
  86. package/dist/agent-sdk/src/runtime-core.d.ts +2 -0
  87. package/dist/agent-sdk/src/runtime-http.d.ts +38 -0
  88. package/dist/agent-sdk/src/runtime-parsers.d.ts +1 -0
  89. package/dist/agent-sdk/src/runtime.d.ts +36 -0
  90. package/dist/agent-sdk/src/types.d.ts +55 -0
  91. package/dist/contracts/src/index.d.ts +889 -12
  92. package/package.json +2 -2
  93. package/src/adapters.ts +385 -36
  94. package/src/index.ts +2 -0
  95. package/src/registry.ts +67 -18
  96. package/src/runtime-core.ts +7 -0
  97. package/src/runtime-http.ts +455 -0
  98. package/src/runtime-parsers.ts +6 -0
  99. package/src/runtime.ts +848 -33
  100. package/src/types.ts +61 -0
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from "./adapters";
2
2
  export * from "./registry";
3
3
  export * from "./runtime";
4
+ export * from "./runtime-core";
5
+ export * from "./runtime-parsers";
4
6
  export * from "./types";
package/src/registry.ts CHANGED
@@ -1,29 +1,70 @@
1
- import {
2
- ClaudeCodeAdapter,
3
- CodexAdapter,
4
- CursorAdapter,
5
- GenericHeartbeatAdapter,
6
- OpenCodeAdapter,
7
- listAdapterModels,
8
- listAdapterMetadata,
9
- testAdapterEnvironment
10
- } from "./adapters";
1
+ import { listAdapterModels, testAdapterEnvironment } from "./adapters";
11
2
  import type {
12
3
  AdapterEnvironmentResult,
13
4
  AdapterMetadata,
5
+ AdapterModule,
14
6
  AdapterModelOption,
15
7
  AgentAdapter,
16
8
  AgentProviderType,
17
9
  AgentRuntimeConfig
18
10
  } from "./types";
11
+ import { codexAdapterModule } from "../../adapters/codex/src";
12
+ import { claudecodeAdapterModule } from "../../adapters/claude-code/src";
13
+ import { cursorAdapterModule } from "../../adapters/cursor/src";
14
+ import { opencodeAdapterModule } from "../../adapters/opencode/src";
15
+ import { openaiapiAdapterModule } from "../../adapters/openai-api/src";
16
+ import { anthropicapiAdapterModule } from "../../adapters/anthropic-api/src";
17
+ import { httpAdapterModule } from "../../adapters/http/src";
18
+ import { shellAdapterModule } from "../../adapters/shell/src";
19
+
20
+ const adapterModules: Record<AgentProviderType, AdapterModule> = {
21
+ claude_code: claudecodeAdapterModule,
22
+ codex: codexAdapterModule,
23
+ cursor: cursorAdapterModule,
24
+ opencode: opencodeAdapterModule,
25
+ openai_api: openaiapiAdapterModule,
26
+ anthropic_api: anthropicapiAdapterModule,
27
+ http: httpAdapterModule,
28
+ shell: shellAdapterModule
29
+ };
30
+
31
+ export function getRegisteredAdapterModules(): Record<AgentProviderType, AdapterModule> {
32
+ return adapterModules;
33
+ }
19
34
 
20
35
  const adapters: Record<AgentProviderType, AgentAdapter> = {
21
- claude_code: new ClaudeCodeAdapter(),
22
- codex: new CodexAdapter(),
23
- cursor: new CursorAdapter(),
24
- opencode: new OpenCodeAdapter(),
25
- http: new GenericHeartbeatAdapter("http"),
26
- shell: new GenericHeartbeatAdapter("shell")
36
+ claude_code: {
37
+ providerType: "claude_code",
38
+ execute: (context) => adapterModules.claude_code.server.execute(context)
39
+ },
40
+ codex: {
41
+ providerType: "codex",
42
+ execute: (context) => adapterModules.codex.server.execute(context)
43
+ },
44
+ cursor: {
45
+ providerType: "cursor",
46
+ execute: (context) => adapterModules.cursor.server.execute(context)
47
+ },
48
+ opencode: {
49
+ providerType: "opencode",
50
+ execute: (context) => adapterModules.opencode.server.execute(context)
51
+ },
52
+ openai_api: {
53
+ providerType: "openai_api",
54
+ execute: (context) => adapterModules.openai_api.server.execute(context)
55
+ },
56
+ anthropic_api: {
57
+ providerType: "anthropic_api",
58
+ execute: (context) => adapterModules.anthropic_api.server.execute(context)
59
+ },
60
+ http: {
61
+ providerType: "http",
62
+ execute: (context) => adapterModules.http.server.execute(context)
63
+ },
64
+ shell: {
65
+ providerType: "shell",
66
+ execute: (context) => adapterModules.shell.server.execute(context)
67
+ }
27
68
  };
28
69
 
29
70
  export function resolveAdapter(providerType: AgentProviderType) {
@@ -34,16 +75,24 @@ export async function getAdapterModels(
34
75
  providerType: AgentProviderType,
35
76
  runtime?: AgentRuntimeConfig
36
77
  ): Promise<AdapterModelOption[]> {
37
- return listAdapterModels(providerType, runtime);
78
+ const fromModule = await adapterModules[providerType].server.listModels?.(runtime);
79
+ if (fromModule) {
80
+ return fromModule;
81
+ }
82
+ return adapterModules[providerType].models ? [...adapterModules[providerType].models] : listAdapterModels(providerType, runtime);
38
83
  }
39
84
 
40
85
  export function getAdapterMetadata(): AdapterMetadata[] {
41
- return listAdapterMetadata();
86
+ return Object.values(adapterModules).map((module) => module.metadata);
42
87
  }
43
88
 
44
89
  export async function runAdapterEnvironmentTest(
45
90
  providerType: AgentProviderType,
46
91
  runtime?: AgentRuntimeConfig
47
92
  ): Promise<AdapterEnvironmentResult> {
93
+ const testEnvironment = adapterModules[providerType].server.testEnvironment;
94
+ if (testEnvironment) {
95
+ return testEnvironment(runtime);
96
+ }
48
97
  return testAdapterEnvironment(providerType, runtime);
49
98
  }
@@ -0,0 +1,7 @@
1
+ export type {
2
+ RuntimeAttemptTrace,
3
+ RuntimeCommandHealth,
4
+ RuntimeExecutionOutput,
5
+ RuntimeTranscriptEvent
6
+ } from "./runtime";
7
+ export { checkRuntimeCommandHealth, executeAgentRuntime, executePromptRuntime } from "./runtime";
@@ -0,0 +1,455 @@
1
+ import type { AgentRuntimeConfig } from "./types";
2
+
3
+ export type DirectApiProvider = "openai_api" | "anthropic_api";
4
+
5
+ export type DirectApiExecutionOutput = {
6
+ ok: boolean;
7
+ provider: DirectApiProvider;
8
+ model: string;
9
+ endpoint: string;
10
+ elapsedMs: number;
11
+ statusCode: number;
12
+ summary?: string;
13
+ tokenInput?: number;
14
+ tokenOutput?: number;
15
+ usdCost?: number;
16
+ failureType?: "auth" | "rate_limit" | "timeout" | "network" | "bad_response" | "http_error";
17
+ error?: string;
18
+ responsePreview?: string;
19
+ attemptCount: number;
20
+ attempts: Array<{
21
+ attempt: number;
22
+ statusCode: number;
23
+ elapsedMs: number;
24
+ failureType?: "auth" | "rate_limit" | "timeout" | "network" | "bad_response" | "http_error";
25
+ error?: string;
26
+ }>;
27
+ };
28
+
29
+ type ProbeResult = {
30
+ ok: boolean;
31
+ statusCode: number;
32
+ elapsedMs: number;
33
+ message: string;
34
+ };
35
+
36
+ const OPENAI_BASE_URL = "https://api.openai.com";
37
+ const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
38
+ const OPENAI_DEFAULT_MODEL = "gpt-5";
39
+ const ANTHROPIC_DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
40
+
41
+ export function resolveDirectApiCredentials(provider: DirectApiProvider, runtime?: AgentRuntimeConfig) {
42
+ if (provider === "openai_api") {
43
+ const key =
44
+ runtime?.env?.OPENAI_API_KEY?.trim() ||
45
+ runtime?.env?.BOPO_OPENAI_API_KEY?.trim() ||
46
+ process.env.OPENAI_API_KEY?.trim() ||
47
+ process.env.BOPO_OPENAI_API_KEY?.trim() ||
48
+ "";
49
+ const baseUrl =
50
+ runtime?.env?.BOPO_OPENAI_BASE_URL?.trim() || process.env.BOPO_OPENAI_BASE_URL?.trim() || OPENAI_BASE_URL;
51
+ return { key, baseUrl };
52
+ }
53
+ const key =
54
+ runtime?.env?.ANTHROPIC_API_KEY?.trim() ||
55
+ runtime?.env?.BOPO_ANTHROPIC_API_KEY?.trim() ||
56
+ process.env.ANTHROPIC_API_KEY?.trim() ||
57
+ process.env.BOPO_ANTHROPIC_API_KEY?.trim() ||
58
+ "";
59
+ const baseUrl =
60
+ runtime?.env?.BOPO_ANTHROPIC_BASE_URL?.trim() || process.env.BOPO_ANTHROPIC_BASE_URL?.trim() || ANTHROPIC_BASE_URL;
61
+ return { key, baseUrl };
62
+ }
63
+
64
+ export async function executeDirectApiRuntime(
65
+ provider: DirectApiProvider,
66
+ prompt: string,
67
+ runtime?: AgentRuntimeConfig
68
+ ): Promise<DirectApiExecutionOutput> {
69
+ const startedAt = Date.now();
70
+ const { key, baseUrl } = resolveDirectApiCredentials(provider, runtime);
71
+ const timeoutMs = runtime?.timeoutMs && runtime.timeoutMs > 0 ? runtime.timeoutMs : 120_000;
72
+ const retryCount = Math.max(0, Math.min(2, runtime?.retryCount ?? 1));
73
+ const retryBackoffMs = Math.max(100, runtime?.retryBackoffMs ?? 400);
74
+ const model = runtime?.model?.trim() || (provider === "openai_api" ? OPENAI_DEFAULT_MODEL : ANTHROPIC_DEFAULT_MODEL);
75
+ const attempts: DirectApiExecutionOutput["attempts"] = [];
76
+
77
+ if (!key) {
78
+ return {
79
+ ok: false,
80
+ provider,
81
+ model,
82
+ endpoint: baseUrl,
83
+ elapsedMs: Date.now() - startedAt,
84
+ statusCode: 0,
85
+ failureType: "auth",
86
+ error: `Missing API key for ${provider}.`,
87
+ attemptCount: 0,
88
+ attempts
89
+ };
90
+ }
91
+
92
+ const endpoint = provider === "openai_api" ? `${stripTrailingSlash(baseUrl)}/v1/responses` : `${stripTrailingSlash(baseUrl)}/v1/messages`;
93
+ const payload =
94
+ provider === "openai_api"
95
+ ? {
96
+ model,
97
+ input: prompt
98
+ }
99
+ : {
100
+ model,
101
+ max_tokens: 4096,
102
+ messages: [{ role: "user", content: prompt }]
103
+ };
104
+ const headers: Record<string, string> =
105
+ provider === "openai_api"
106
+ ? {
107
+ "content-type": "application/json",
108
+ authorization: `Bearer ${key}`
109
+ }
110
+ : {
111
+ "content-type": "application/json",
112
+ "x-api-key": key,
113
+ "anthropic-version": "2023-06-01"
114
+ };
115
+
116
+ const maxAttempts = 1 + retryCount;
117
+ let lastFailure: Omit<DirectApiExecutionOutput, "ok" | "provider" | "model" | "endpoint" | "attemptCount" | "attempts"> | null = null;
118
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
119
+ const attemptStartedAt = Date.now();
120
+ try {
121
+ const response = await fetchWithTimeout(
122
+ endpoint,
123
+ {
124
+ method: "POST",
125
+ headers,
126
+ body: JSON.stringify(payload)
127
+ },
128
+ timeoutMs
129
+ );
130
+ const elapsedMs = Date.now() - startedAt;
131
+ const attemptElapsedMs = Date.now() - attemptStartedAt;
132
+ const text = await response.text();
133
+ const preview = toPreview(text);
134
+ const parsed = tryParseJson(text);
135
+ if (!response.ok) {
136
+ const failureType = classifyHttpFailure(response.status);
137
+ const error = extractErrorMessage(provider, parsed, text) || `HTTP ${response.status}`;
138
+ attempts.push({
139
+ attempt,
140
+ statusCode: response.status,
141
+ elapsedMs: attemptElapsedMs,
142
+ failureType,
143
+ error
144
+ });
145
+ lastFailure = {
146
+ elapsedMs,
147
+ statusCode: response.status,
148
+ failureType,
149
+ error,
150
+ responsePreview: preview
151
+ };
152
+ if (!isRetryableFailure(failureType, response.status) || attempt >= maxAttempts) {
153
+ break;
154
+ }
155
+ await sleep(retryBackoffMs * attempt);
156
+ continue;
157
+ }
158
+ if (!parsed || typeof parsed !== "object") {
159
+ const failureType = "bad_response" as const;
160
+ const error = "Provider returned non-JSON response.";
161
+ attempts.push({
162
+ attempt,
163
+ statusCode: response.status,
164
+ elapsedMs: attemptElapsedMs,
165
+ failureType,
166
+ error
167
+ });
168
+ lastFailure = {
169
+ elapsedMs,
170
+ statusCode: response.status,
171
+ failureType,
172
+ error,
173
+ responsePreview: preview
174
+ };
175
+ break;
176
+ }
177
+ const summary = provider === "openai_api" ? extractOpenAiSummary(parsed) : extractAnthropicSummary(parsed);
178
+ const usage = provider === "openai_api" ? extractOpenAiUsage(parsed) : extractAnthropicUsage(parsed);
179
+ const estimatedCost = estimateCostFromRates(provider, usage.tokenInput, usage.tokenOutput, runtime);
180
+ attempts.push({
181
+ attempt,
182
+ statusCode: response.status,
183
+ elapsedMs: attemptElapsedMs
184
+ });
185
+ return {
186
+ ok: true,
187
+ provider,
188
+ model,
189
+ endpoint,
190
+ elapsedMs,
191
+ statusCode: response.status,
192
+ summary,
193
+ tokenInput: usage.tokenInput,
194
+ tokenOutput: usage.tokenOutput,
195
+ usdCost: usage.usdCost > 0 ? usage.usdCost : estimatedCost,
196
+ responsePreview: preview,
197
+ attemptCount: attempts.length,
198
+ attempts
199
+ };
200
+ } catch (error) {
201
+ const elapsedMs = Date.now() - startedAt;
202
+ const attemptElapsedMs = Date.now() - attemptStartedAt;
203
+ const failureType = isAbortTimeoutError(error) ? "timeout" : "network";
204
+ const message =
205
+ failureType === "timeout" ? `Request timed out after ${timeoutMs}ms.` : `Network failure: ${String(error)}`;
206
+ attempts.push({
207
+ attempt,
208
+ statusCode: 0,
209
+ elapsedMs: attemptElapsedMs,
210
+ failureType,
211
+ error: message
212
+ });
213
+ lastFailure = {
214
+ elapsedMs,
215
+ statusCode: 0,
216
+ failureType,
217
+ error: message
218
+ };
219
+ if (!isRetryableFailure(failureType, 0) || attempt >= maxAttempts) {
220
+ break;
221
+ }
222
+ await sleep(retryBackoffMs * attempt);
223
+ }
224
+ }
225
+ return {
226
+ ok: false,
227
+ provider,
228
+ model,
229
+ endpoint,
230
+ elapsedMs: lastFailure?.elapsedMs ?? Date.now() - startedAt,
231
+ statusCode: lastFailure?.statusCode ?? 0,
232
+ failureType: lastFailure?.failureType ?? "network",
233
+ error: lastFailure?.error ?? "Direct API request failed.",
234
+ responsePreview: lastFailure?.responsePreview,
235
+ attemptCount: attempts.length,
236
+ attempts
237
+ };
238
+ }
239
+
240
+ export async function probeDirectApiEnvironment(
241
+ provider: DirectApiProvider,
242
+ runtime?: AgentRuntimeConfig
243
+ ): Promise<ProbeResult> {
244
+ const startedAt = Date.now();
245
+ const { key, baseUrl } = resolveDirectApiCredentials(provider, runtime);
246
+ if (!key) {
247
+ return {
248
+ ok: false,
249
+ statusCode: 0,
250
+ elapsedMs: Date.now() - startedAt,
251
+ message: "API key missing."
252
+ };
253
+ }
254
+ const endpoint = provider === "openai_api" ? `${stripTrailingSlash(baseUrl)}/v1/models` : `${stripTrailingSlash(baseUrl)}/v1/models`;
255
+ const headers: Record<string, string> =
256
+ provider === "openai_api"
257
+ ? { authorization: `Bearer ${key}` }
258
+ : { "x-api-key": key, "anthropic-version": "2023-06-01" };
259
+ try {
260
+ const response = await fetchWithTimeout(endpoint, { method: "GET", headers }, 5_000);
261
+ const elapsedMs = Date.now() - startedAt;
262
+ return {
263
+ ok: response.ok,
264
+ statusCode: response.status,
265
+ elapsedMs,
266
+ message: response.ok ? "API probe succeeded." : `API probe returned HTTP ${response.status}.`
267
+ };
268
+ } catch (error) {
269
+ const elapsedMs = Date.now() - startedAt;
270
+ if (isAbortTimeoutError(error)) {
271
+ return {
272
+ ok: false,
273
+ statusCode: 0,
274
+ elapsedMs,
275
+ message: "API probe timed out."
276
+ };
277
+ }
278
+ return {
279
+ ok: false,
280
+ statusCode: 0,
281
+ elapsedMs,
282
+ message: `API probe failed: ${String(error)}`
283
+ };
284
+ }
285
+ }
286
+
287
+ async function fetchWithTimeout(url: string, init: RequestInit, timeoutMs: number) {
288
+ const controller = new AbortController();
289
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
290
+ try {
291
+ return await fetch(url, { ...init, signal: controller.signal });
292
+ } finally {
293
+ clearTimeout(timeout);
294
+ }
295
+ }
296
+
297
+ function extractOpenAiSummary(parsed: Record<string, unknown>) {
298
+ const outputText = parsed.output_text;
299
+ if (typeof outputText === "string" && outputText.trim()) return outputText.trim();
300
+ const output = parsed.output;
301
+ if (Array.isArray(output)) {
302
+ for (const item of output) {
303
+ if (!item || typeof item !== "object") continue;
304
+ const content = (item as Record<string, unknown>).content;
305
+ if (!Array.isArray(content)) continue;
306
+ for (const block of content) {
307
+ if (!block || typeof block !== "object") continue;
308
+ const blockText = (block as Record<string, unknown>).text;
309
+ if (typeof blockText === "string" && blockText.trim()) return blockText.trim();
310
+ }
311
+ }
312
+ }
313
+ return "OpenAI API request completed.";
314
+ }
315
+
316
+ function extractAnthropicSummary(parsed: Record<string, unknown>) {
317
+ const content = parsed.content;
318
+ if (!Array.isArray(content)) return "Anthropic API request completed.";
319
+ const texts: string[] = [];
320
+ for (const entry of content) {
321
+ if (!entry || typeof entry !== "object") continue;
322
+ const block = entry as Record<string, unknown>;
323
+ if (block.type === "text" && typeof block.text === "string" && block.text.trim()) {
324
+ texts.push(block.text.trim());
325
+ }
326
+ }
327
+ return texts.join("\n\n").trim() || "Anthropic API request completed.";
328
+ }
329
+
330
+ function extractOpenAiUsage(parsed: Record<string, unknown>) {
331
+ const usage = parsed.usage;
332
+ if (!usage || typeof usage !== "object") return { tokenInput: 0, tokenOutput: 0, usdCost: 0 };
333
+ const record = usage as Record<string, unknown>;
334
+ const tokenInput = toNumber(record.input_tokens) ?? toNumber(record.prompt_tokens) ?? 0;
335
+ const tokenOutput = toNumber(record.output_tokens) ?? toNumber(record.completion_tokens) ?? 0;
336
+ const usdCost = toNumber(record.cost_usd) ?? toNumber(record.total_cost_usd) ?? 0;
337
+ return { tokenInput, tokenOutput, usdCost };
338
+ }
339
+
340
+ function extractAnthropicUsage(parsed: Record<string, unknown>) {
341
+ const usage = parsed.usage;
342
+ if (!usage || typeof usage !== "object") return { tokenInput: 0, tokenOutput: 0, usdCost: 0 };
343
+ const record = usage as Record<string, unknown>;
344
+ const tokenInput = (toNumber(record.input_tokens) ?? 0) + (toNumber(record.cache_read_input_tokens) ?? 0);
345
+ const tokenOutput = toNumber(record.output_tokens) ?? 0;
346
+ const usdCost = toNumber(record.cost_usd) ?? toNumber(record.total_cost_usd) ?? 0;
347
+ return { tokenInput, tokenOutput, usdCost };
348
+ }
349
+
350
+ function extractErrorMessage(provider: DirectApiProvider, parsed: Record<string, unknown> | null, fallback: string) {
351
+ const fallbackMessage = toPreview(fallback, 320);
352
+ if (!parsed) return fallbackMessage;
353
+ const error = parsed.error;
354
+ if (typeof error === "string" && error.trim()) return error.trim();
355
+ if (error && typeof error === "object") {
356
+ const errorRecord = error as Record<string, unknown>;
357
+ const candidates = [
358
+ errorRecord.message,
359
+ errorRecord.error?.toString(),
360
+ (errorRecord.details as string | undefined) ?? undefined
361
+ ];
362
+ for (const candidate of candidates) {
363
+ if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
364
+ }
365
+ }
366
+ if (provider === "openai_api" && typeof parsed.message === "string" && parsed.message.trim()) {
367
+ return parsed.message.trim();
368
+ }
369
+ return fallbackMessage;
370
+ }
371
+
372
+ function classifyHttpFailure(statusCode: number): "auth" | "rate_limit" | "http_error" {
373
+ if (statusCode === 401 || statusCode === 403) return "auth";
374
+ if (statusCode === 429) return "rate_limit";
375
+ return "http_error";
376
+ }
377
+
378
+ function isRetryableFailure(
379
+ failureType: NonNullable<DirectApiExecutionOutput["failureType"]>,
380
+ statusCode: number
381
+ ) {
382
+ if (failureType === "timeout" || failureType === "network" || failureType === "rate_limit") {
383
+ return true;
384
+ }
385
+ if (failureType === "http_error" && statusCode >= 500) {
386
+ return true;
387
+ }
388
+ return false;
389
+ }
390
+
391
+ function stripTrailingSlash(value: string) {
392
+ return value.endsWith("/") ? value.slice(0, -1) : value;
393
+ }
394
+
395
+ function tryParseJson(text: string): Record<string, unknown> | null {
396
+ try {
397
+ const parsed = JSON.parse(text) as unknown;
398
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
399
+ return parsed as Record<string, unknown>;
400
+ }
401
+ } catch {
402
+ // ignore
403
+ }
404
+ return null;
405
+ }
406
+
407
+ function isAbortTimeoutError(error: unknown) {
408
+ if (!error || typeof error !== "object") return false;
409
+ const maybeError = error as { name?: string };
410
+ return maybeError.name === "AbortError";
411
+ }
412
+
413
+ function toPreview(value: string, max = 1600) {
414
+ const normalized = value.trim();
415
+ if (normalized.length <= max) return normalized;
416
+ return `${normalized.slice(0, max)}\n...[truncated]`;
417
+ }
418
+
419
+ function toNumber(value: unknown) {
420
+ if (typeof value === "number" && Number.isFinite(value)) return value;
421
+ if (typeof value === "string") {
422
+ const parsed = Number(value);
423
+ if (Number.isFinite(parsed)) return parsed;
424
+ }
425
+ return undefined;
426
+ }
427
+
428
+ function estimateCostFromRates(
429
+ provider: DirectApiProvider,
430
+ tokenInput: number,
431
+ tokenOutput: number,
432
+ runtime?: AgentRuntimeConfig
433
+ ) {
434
+ const env = runtime?.env ?? {};
435
+ const inputRate =
436
+ toNumber(
437
+ provider === "openai_api"
438
+ ? env.BOPO_OPENAI_INPUT_USD_PER_1M ?? process.env.BOPO_OPENAI_INPUT_USD_PER_1M
439
+ : env.BOPO_ANTHROPIC_INPUT_USD_PER_1M ?? process.env.BOPO_ANTHROPIC_INPUT_USD_PER_1M
440
+ ) ?? 0;
441
+ const outputRate =
442
+ toNumber(
443
+ provider === "openai_api"
444
+ ? env.BOPO_OPENAI_OUTPUT_USD_PER_1M ?? process.env.BOPO_OPENAI_OUTPUT_USD_PER_1M
445
+ : env.BOPO_ANTHROPIC_OUTPUT_USD_PER_1M ?? process.env.BOPO_ANTHROPIC_OUTPUT_USD_PER_1M
446
+ ) ?? 0;
447
+ if (inputRate <= 0 && outputRate <= 0) {
448
+ return 0;
449
+ }
450
+ return Number((((tokenInput * inputRate) + (tokenOutput * outputRate)) / 1_000_000).toFixed(6));
451
+ }
452
+
453
+ function sleep(ms: number) {
454
+ return new Promise<void>((resolve) => setTimeout(resolve, ms));
455
+ }
@@ -0,0 +1,6 @@
1
+ export {
2
+ parseClaudeStreamOutput,
3
+ parseCursorStreamOutput,
4
+ parseRuntimeTranscript,
5
+ parseStructuredUsage
6
+ } from "./runtime";