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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/adapters/anthropic-api/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/anthropic-api/src/cli/index.d.ts +1 -0
- package/dist/adapters/anthropic-api/src/index.d.ts +16 -0
- package/dist/adapters/anthropic-api/src/server/execute.d.ts +2 -0
- package/dist/adapters/anthropic-api/src/server/index.d.ts +6 -0
- package/dist/adapters/anthropic-api/src/server/parse.d.ts +1 -0
- package/dist/adapters/anthropic-api/src/server/test.d.ts +2 -0
- package/dist/adapters/anthropic-api/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/anthropic-api/src/ui/index.d.ts +2 -0
- package/dist/adapters/anthropic-api/src/ui/parse-stdout.d.ts +6 -0
- package/dist/adapters/claude-code/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/claude-code/src/cli/index.d.ts +1 -0
- package/dist/adapters/claude-code/src/index.d.ts +16 -0
- package/dist/adapters/claude-code/src/server/execute.d.ts +2 -0
- package/dist/adapters/claude-code/src/server/index.d.ts +6 -0
- package/dist/adapters/claude-code/src/server/parse.d.ts +2 -0
- package/dist/adapters/claude-code/src/server/test.d.ts +2 -0
- package/dist/adapters/claude-code/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/claude-code/src/ui/index.d.ts +2 -0
- package/dist/adapters/claude-code/src/ui/parse-stdout.d.ts +6 -0
- package/dist/adapters/codex/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/codex/src/cli/index.d.ts +1 -0
- package/dist/adapters/codex/src/index.d.ts +34 -0
- package/dist/adapters/codex/src/server/execute.d.ts +2 -0
- package/dist/adapters/codex/src/server/index.d.ts +6 -0
- package/dist/adapters/codex/src/server/parse.d.ts +2 -0
- package/dist/adapters/codex/src/server/test.d.ts +2 -0
- package/dist/adapters/codex/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/codex/src/ui/index.d.ts +2 -0
- package/dist/adapters/codex/src/ui/parse-stdout.d.ts +6 -0
- package/dist/adapters/cursor/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/cursor/src/cli/index.d.ts +1 -0
- package/dist/adapters/cursor/src/index.d.ts +22 -0
- package/dist/adapters/cursor/src/server/execute.d.ts +2 -0
- package/dist/adapters/cursor/src/server/index.d.ts +6 -0
- package/dist/adapters/cursor/src/server/parse.d.ts +2 -0
- package/dist/adapters/cursor/src/server/test.d.ts +2 -0
- package/dist/adapters/cursor/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/cursor/src/ui/index.d.ts +2 -0
- package/dist/adapters/cursor/src/ui/parse-stdout.d.ts +6 -0
- package/dist/adapters/http/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/http/src/cli/index.d.ts +1 -0
- package/dist/adapters/http/src/index.d.ts +7 -0
- package/dist/adapters/http/src/server/execute.d.ts +2 -0
- package/dist/adapters/http/src/server/index.d.ts +6 -0
- package/dist/adapters/http/src/server/parse.d.ts +1 -0
- package/dist/adapters/http/src/server/test.d.ts +2 -0
- package/dist/adapters/http/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/http/src/ui/index.d.ts +2 -0
- package/dist/adapters/http/src/ui/parse-stdout.d.ts +6 -0
- package/dist/adapters/openai-api/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/openai-api/src/cli/index.d.ts +1 -0
- package/dist/adapters/openai-api/src/index.d.ts +22 -0
- package/dist/adapters/openai-api/src/server/execute.d.ts +2 -0
- package/dist/adapters/openai-api/src/server/index.d.ts +6 -0
- package/dist/adapters/openai-api/src/server/parse.d.ts +1 -0
- package/dist/adapters/openai-api/src/server/test.d.ts +2 -0
- package/dist/adapters/openai-api/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/openai-api/src/ui/index.d.ts +2 -0
- package/dist/adapters/openai-api/src/ui/parse-stdout.d.ts +6 -0
- package/dist/adapters/opencode/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/opencode/src/cli/index.d.ts +1 -0
- package/dist/adapters/opencode/src/index.d.ts +7 -0
- package/dist/adapters/opencode/src/server/execute.d.ts +2 -0
- package/dist/adapters/opencode/src/server/index.d.ts +6 -0
- package/dist/adapters/opencode/src/server/parse.d.ts +1 -0
- package/dist/adapters/opencode/src/server/test.d.ts +2 -0
- package/dist/adapters/opencode/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/opencode/src/ui/index.d.ts +2 -0
- package/dist/adapters/opencode/src/ui/parse-stdout.d.ts +6 -0
- package/dist/adapters/shell/src/cli/format-event.d.ts +1 -0
- package/dist/adapters/shell/src/cli/index.d.ts +1 -0
- package/dist/adapters/shell/src/index.d.ts +7 -0
- package/dist/adapters/shell/src/server/execute.d.ts +2 -0
- package/dist/adapters/shell/src/server/index.d.ts +6 -0
- package/dist/adapters/shell/src/server/parse.d.ts +1 -0
- package/dist/adapters/shell/src/server/test.d.ts +2 -0
- package/dist/adapters/shell/src/ui/build-config.d.ts +3 -0
- package/dist/adapters/shell/src/ui/index.d.ts +2 -0
- package/dist/adapters/shell/src/ui/parse-stdout.d.ts +6 -0
- package/dist/agent-sdk/src/adapters.d.ts +226 -1
- package/dist/agent-sdk/src/index.d.ts +2 -0
- package/dist/agent-sdk/src/registry.d.ts +2 -1
- package/dist/agent-sdk/src/runtime-core.d.ts +2 -0
- package/dist/agent-sdk/src/runtime-http.d.ts +38 -0
- package/dist/agent-sdk/src/runtime-parsers.d.ts +1 -0
- package/dist/agent-sdk/src/runtime.d.ts +36 -0
- package/dist/agent-sdk/src/types.d.ts +55 -0
- package/dist/contracts/src/index.d.ts +889 -12
- package/package.json +2 -2
- package/src/adapters.ts +385 -36
- package/src/index.ts +2 -0
- package/src/registry.ts +67 -18
- package/src/runtime-core.ts +7 -0
- package/src/runtime-http.ts +455 -0
- package/src/runtime-parsers.ts +6 -0
- package/src/runtime.ts +848 -33
- package/src/types.ts +61 -0
package/src/index.ts
CHANGED
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:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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,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
|
+
}
|