@springbrand/agent-runtime 0.3.0-alpha.22 → 0.3.0-alpha.24
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/canonical-context.d.ts +661 -0
- package/dist/canonical-context.js +122 -0
- package/package.json +14 -5
- package/scripts/build-canonical-context.mjs +38 -0
- package/src/adapter/cloudflare/agent.ts +2 -2
- package/src/adapter/cloudflare/define-agent.ts +3 -5
- package/src/config/ports.ts +2 -3
- package/src/config/public-contracts.ts +1 -0
- package/src/conversation/canonical-context.ts +144 -0
- package/src/conversation/history.ts +41 -45
- package/src/conversation/message/contract.ts +2 -11
- package/src/conversation/message/conversion.ts +3 -5
- package/src/conversation/message/user-sidecar.ts +7 -0
- package/src/conversation/native-extensions.ts +13 -0
- package/src/index.ts +1 -9
- package/src/models/index.ts +23 -40
- package/src/models/openrouter/messages.ts +5 -8
- package/src/models/openrouter/openai.ts +3 -7
- package/src/models/openrouter/protocol-response.ts +3 -15
- package/src/submission/steer-delivery.ts +4 -2
- package/src/tools/web/search/api.ts +6 -1
- package/src/model-protocol/openrouter.ts +0 -41
package/src/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ export type { RuntimeAssemblyView } from "./config/assembly-view";
|
|
|
51
51
|
export * from "./adapter/cloudflare/define-agent";
|
|
52
52
|
export type { ToolInteractionResponsePreparation } from "./adapter/cloudflare/agent";
|
|
53
53
|
export * from "./conversation/state";
|
|
54
|
+
export type { ModelProtocolResponse } from "./conversation/canonical-context";
|
|
54
55
|
export * from "./tools/workspace/versioning";
|
|
55
56
|
export * from "./lib/execution-level";
|
|
56
57
|
export type {
|
|
@@ -144,12 +145,3 @@ export {
|
|
|
144
145
|
modelRequestUrl,
|
|
145
146
|
resolvePiModel,
|
|
146
147
|
} from "./models";
|
|
147
|
-
export {
|
|
148
|
-
openRouterRequestUrl,
|
|
149
|
-
requestOpenRouter,
|
|
150
|
-
} from "./model-protocol/openrouter";
|
|
151
|
-
export type { OpenRouterRequestOptions } from "./model-protocol/openrouter";
|
|
152
|
-
export type {
|
|
153
|
-
ModelProtocolResponse,
|
|
154
|
-
OpenRouterProtocol,
|
|
155
|
-
} from "./models/openrouter/protocol-response";
|
package/src/models/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import {
|
|
3
|
+
openRouterRequestUrl,
|
|
4
|
+
withOpenRouterStructuredOutput,
|
|
5
|
+
type OpenRouterGenerationProtocol,
|
|
6
|
+
} from "@springbrand/model-client";
|
|
2
7
|
import {
|
|
3
8
|
createModels,
|
|
4
9
|
createProvider,
|
|
@@ -148,18 +153,23 @@ type ProviderBoundStream = (
|
|
|
148
153
|
options?: SimpleStreamOptions,
|
|
149
154
|
) => ReturnType<StreamFn>;
|
|
150
155
|
|
|
156
|
+
function openRouterProtocolForModel(model: Model<Api>): OpenRouterGenerationProtocol | undefined {
|
|
157
|
+
if (model.api === "anthropic-messages" &&
|
|
158
|
+
model.provider.startsWith("model-api-openrouter-messages-")) return "openrouter-messages";
|
|
159
|
+
if (model.api === "openai-completions" &&
|
|
160
|
+
model.provider.startsWith("model-api-openrouter-chat-")) return "openrouter-chat";
|
|
161
|
+
if (model.api === "openai-responses" &&
|
|
162
|
+
model.provider.startsWith("model-api-openrouter-responses-")) return "openrouter-responses";
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
151
166
|
export function structuredOutputStream(
|
|
152
167
|
streamFn: StreamFn,
|
|
153
168
|
model: Model<Api>,
|
|
154
169
|
schema: Record<string, unknown>,
|
|
155
170
|
): StreamFn | undefined {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
const chat = model.api === "openai-completions" &&
|
|
159
|
-
model.provider.startsWith("model-api-openrouter-chat-");
|
|
160
|
-
const responses = model.api === "openai-responses" &&
|
|
161
|
-
model.provider.startsWith("model-api-openrouter-responses-");
|
|
162
|
-
if (!messages && !chat && !responses) return undefined;
|
|
171
|
+
const protocol = openRouterProtocolForModel(model);
|
|
172
|
+
if (!protocol) return undefined;
|
|
163
173
|
|
|
164
174
|
return (activeModel, context, options) =>
|
|
165
175
|
streamFn(activeModel, context, {
|
|
@@ -169,38 +179,9 @@ export function structuredOutputStream(
|
|
|
169
179
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
170
180
|
throw new Error("Structured model payload must be an object");
|
|
171
181
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
...body,
|
|
177
|
-
output_config: {
|
|
178
|
-
...(current && typeof current === "object" && !Array.isArray(current)
|
|
179
|
-
? current
|
|
180
|
-
: {}),
|
|
181
|
-
format: { type: "json_schema", schema },
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
if (chat) {
|
|
186
|
-
return {
|
|
187
|
-
...body,
|
|
188
|
-
response_format: {
|
|
189
|
-
type: "json_schema",
|
|
190
|
-
json_schema: { name: "agent_output", strict: true, schema },
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
const current = body.text;
|
|
195
|
-
return {
|
|
196
|
-
...body,
|
|
197
|
-
text: {
|
|
198
|
-
...(current && typeof current === "object" && !Array.isArray(current)
|
|
199
|
-
? current
|
|
200
|
-
: {}),
|
|
201
|
-
format: { type: "json_schema", name: "agent_output", strict: true, schema },
|
|
202
|
-
},
|
|
203
|
-
};
|
|
182
|
+
return withOpenRouterStructuredOutput(
|
|
183
|
+
value as Record<string, unknown>, protocol, schema, "agent_output",
|
|
184
|
+
);
|
|
204
185
|
},
|
|
205
186
|
});
|
|
206
187
|
}
|
|
@@ -506,6 +487,8 @@ function trimTrailingSlash(value: string): string {
|
|
|
506
487
|
* Codex 走 `resolveCodexUrl`。不要在这里"顺手规范化"路径,否则日志会与真实请求脱节,反而掩盖配置错误。
|
|
507
488
|
*/
|
|
508
489
|
export function modelRequestUrl(model: Model<Api>): string {
|
|
490
|
+
const protocol = openRouterProtocolForModel(model);
|
|
491
|
+
if (protocol) return openRouterRequestUrl({ protocol, baseURL: model.baseUrl ?? "" });
|
|
509
492
|
const base = trimTrailingSlash(model.baseUrl ?? "");
|
|
510
493
|
switch (model.api) {
|
|
511
494
|
case "openai-completions":
|
|
@@ -766,7 +749,7 @@ function piApiFor(endpoint: RuntimeModelEndpoint): ProviderStreams {
|
|
|
766
749
|
}
|
|
767
750
|
})();
|
|
768
751
|
const api = protocol.startsWith("openrouter-")
|
|
769
|
-
? withModelProtocolResponse(implementation, protocol as
|
|
752
|
+
? withModelProtocolResponse(implementation, protocol as OpenRouterGenerationProtocol)
|
|
770
753
|
: implementation;
|
|
771
754
|
const contextFor = (
|
|
772
755
|
model: Model<Api>,
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TranscriptContext,
|
|
6
6
|
} from "@earendil-works/pi-ai";
|
|
7
7
|
import { getCurrentTools } from "@earendil-works/pi-ai";
|
|
8
|
+
import { createOpenRouterFetch, openRouterHeaders, withOpenRouterRouting } from "@springbrand/model-client";
|
|
8
9
|
import {
|
|
9
10
|
anthropicMessagesApi,
|
|
10
11
|
} from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
|
|
@@ -60,13 +61,7 @@ function decoratePayload(
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
return {
|
|
63
|
-
...body,
|
|
64
|
-
...(sessionId ? { session_id: sessionId } : {}),
|
|
65
|
-
provider: {
|
|
66
|
-
...record(body.provider),
|
|
67
|
-
...model.compat?.openRouterRouting,
|
|
68
|
-
require_parameters: true,
|
|
69
|
-
},
|
|
64
|
+
...withOpenRouterRouting(body, { ...model.compat?.openRouterRouting }, sessionId),
|
|
70
65
|
...(deferred.length > 0
|
|
71
66
|
? {
|
|
72
67
|
tools: [
|
|
@@ -96,12 +91,14 @@ function openRouterOptions(
|
|
|
96
91
|
return {
|
|
97
92
|
...options,
|
|
98
93
|
apiKey: undefined,
|
|
94
|
+
fetch: createOpenRouterFetch({ apiKey, fetch: options?.fetch }),
|
|
99
95
|
headers: {
|
|
96
|
+
// Pi uses null to suppress SDK/model defaults before building native headers.
|
|
100
97
|
...headers,
|
|
98
|
+
...Object.fromEntries(openRouterHeaders(apiKey)),
|
|
101
99
|
...(model.compat?.supportsStrictTools === true
|
|
102
100
|
? { "x-anthropic-beta": STRUCTURED_OUTPUTS_BETA }
|
|
103
101
|
: {}),
|
|
104
|
-
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
105
102
|
"x-api-key": null,
|
|
106
103
|
},
|
|
107
104
|
onPayload: async (payload, activeModel) => {
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
TranscriptContext,
|
|
7
7
|
} from "@earendil-works/pi-ai";
|
|
8
8
|
import { getCurrentTools } from "@earendil-works/pi-ai";
|
|
9
|
+
import { createOpenRouterFetch, withOpenRouterRouting } from "@springbrand/model-client";
|
|
9
10
|
import {
|
|
10
11
|
openAICompletionsApi,
|
|
11
12
|
} from "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
@@ -27,6 +28,7 @@ function openRouterOptions(
|
|
|
27
28
|
): StreamOptions {
|
|
28
29
|
return {
|
|
29
30
|
...options,
|
|
31
|
+
fetch: createOpenRouterFetch({ apiKey: options?.apiKey, fetch: options?.fetch }),
|
|
30
32
|
onPayload: async (payload, activeModel) => {
|
|
31
33
|
const callerPayload = await options?.onPayload?.(payload, activeModel) ?? payload;
|
|
32
34
|
const body = record(callerPayload);
|
|
@@ -48,18 +50,12 @@ function openRouterOptions(
|
|
|
48
50
|
const reasoning = record(replayed.reasoning);
|
|
49
51
|
const include = Array.isArray(replayed.include) ? replayed.include : [];
|
|
50
52
|
return {
|
|
51
|
-
...replayed,
|
|
53
|
+
...withOpenRouterRouting(replayed, compat?.openRouterRouting, options?.sessionId),
|
|
52
54
|
...(model.api === "openai-responses" && reasoning?.effort !== undefined &&
|
|
53
55
|
reasoning.effort !== null && reasoning.effort !== "none" &&
|
|
54
56
|
!include.includes("reasoning.encrypted_content")
|
|
55
57
|
? { include: [...include, "reasoning.encrypted_content"] }
|
|
56
58
|
: {}),
|
|
57
|
-
...(options?.sessionId ? { session_id: options.sessionId } : {}),
|
|
58
|
-
provider: {
|
|
59
|
-
...record(replayed.provider),
|
|
60
|
-
...compat?.openRouterRouting,
|
|
61
|
-
require_parameters: true,
|
|
62
|
-
},
|
|
63
59
|
};
|
|
64
60
|
},
|
|
65
61
|
};
|
|
@@ -7,21 +7,9 @@ import {
|
|
|
7
7
|
type ProviderStreams,
|
|
8
8
|
type StreamOptions,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
|
-
import type {
|
|
10
|
+
import type { OpenRouterGenerationProtocol } from "@springbrand/model-client";
|
|
11
11
|
import { overlayProviderStreams } from "../provider-streams";
|
|
12
|
-
|
|
13
|
-
export type OpenRouterProtocol = Extract<RuntimeModelProtocol, `openrouter-${string}`>;
|
|
14
|
-
|
|
15
|
-
/** Native output required by OpenRouter's stateless continuation. */
|
|
16
|
-
export interface ModelProtocolResponse {
|
|
17
|
-
readonly output: readonly Readonly<Record<string, unknown>>[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
declare module "@earendil-works/pi-ai" {
|
|
21
|
-
interface AssistantMessage {
|
|
22
|
-
protocolResponse?: ModelProtocolResponse;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
12
|
+
import type { ModelProtocolResponse } from "../../conversation/canonical-context";
|
|
25
13
|
|
|
26
14
|
function record(value: unknown): Record<string, unknown> | undefined {
|
|
27
15
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
@@ -73,7 +61,7 @@ function streamWithProtocolResponse<T extends StreamOptions>(
|
|
|
73
61
|
|
|
74
62
|
export function withModelProtocolResponse(
|
|
75
63
|
api: ProviderStreams,
|
|
76
|
-
protocol:
|
|
64
|
+
protocol: OpenRouterGenerationProtocol,
|
|
77
65
|
): ProviderStreams {
|
|
78
66
|
if (protocol !== "openrouter-responses") return api;
|
|
79
67
|
return overlayProviderStreams(api, {
|
|
@@ -6,6 +6,7 @@ import type { SubmitMessageOptions } from "./admission";
|
|
|
6
6
|
import type { SubmissionHandle } from "./lifecycle";
|
|
7
7
|
import { submissionAbortReason, type StoredSubmission, type SubmissionRepository } from "./repository";
|
|
8
8
|
import type { PiCanonicalUserInput } from "../conversation/message/contract";
|
|
9
|
+
import { captureUIUserSidecar } from "../conversation/message/conversion";
|
|
9
10
|
import type { MessageUiRepository } from "../conversation/repository";
|
|
10
11
|
import type { PendingSteer, SteerRepository } from "./steer-repository";
|
|
11
12
|
|
|
@@ -70,6 +71,7 @@ export async function finishPendingSteers(options: {
|
|
|
70
71
|
const userMessage = steer.uiMessageJson
|
|
71
72
|
? JSON.parse(steer.uiMessageJson) as UIMessage & { role: "user" }
|
|
72
73
|
: undefined;
|
|
74
|
+
const userSidecar = userMessage ? { id: userMessage.id, ...captureUIUserSidecar(userMessage) } : undefined;
|
|
73
75
|
const requestId = `${options.submissionId}:steer:${steer.steerId}`;
|
|
74
76
|
if (options.aborted) {
|
|
75
77
|
const owner = options.transaction(() => {
|
|
@@ -81,7 +83,7 @@ export async function finishPendingSteers(options: {
|
|
|
81
83
|
}
|
|
82
84
|
return handoff?.status === "aborted" || handoff?.status === "skipped" ? handoff.submissionId : options.submissionId;
|
|
83
85
|
});
|
|
84
|
-
await options.conversation.append(steer.messageId, message, { submissionId: owner, ...(
|
|
86
|
+
await options.conversation.append(steer.messageId, message, { submissionId: owner, ...(userSidecar ? { userSidecar } : {}) });
|
|
85
87
|
options.transaction(() => {
|
|
86
88
|
if (steer.harnessEntryId) options.messageUi.delete(steer.harnessEntryId);
|
|
87
89
|
options.steers.deleteByMessageId(steer.messageId);
|
|
@@ -117,7 +119,7 @@ export async function finishPendingSteers(options: {
|
|
|
117
119
|
}
|
|
118
120
|
return current;
|
|
119
121
|
});
|
|
120
|
-
if (handoff.status === "skipped" || handoff.status === "aborted") await options.conversation.append(steer.messageId, message, { submissionId: handoff.submissionId, ...(
|
|
122
|
+
if (handoff.status === "skipped" || handoff.status === "aborted") await options.conversation.append(steer.messageId, message, { submissionId: handoff.submissionId, ...(userSidecar ? { userSidecar } : {}) });
|
|
121
123
|
options.transaction(() => options.steers.deleteByMessageId(steer.messageId));
|
|
122
124
|
}
|
|
123
125
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { RuntimeModelEndpoint } from "../../../config/ports";
|
|
9
|
+
import { createOpenRouterFetch, openRouterRequestUrl } from "@springbrand/model-client";
|
|
9
10
|
|
|
10
11
|
export interface WebSearchSource {
|
|
11
12
|
title: string;
|
|
@@ -295,6 +296,9 @@ function isOpenRouterModel(model: NativeWebSearchModel): boolean {
|
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
function resolveOpenAIResponsesUrl(model: NativeWebSearchModel): string {
|
|
299
|
+
if (isOpenRouterModel(model)) {
|
|
300
|
+
return openRouterRequestUrl({ protocol: "openrouter-responses", baseURL: model.baseUrl });
|
|
301
|
+
}
|
|
298
302
|
const base = trimTrailingSlash(model.baseUrl);
|
|
299
303
|
if (!isOpenAICodexModel(model)) return `${base}/responses`;
|
|
300
304
|
if (base.endsWith("/codex/responses")) return base;
|
|
@@ -733,7 +737,8 @@ async function callOpenAIStream(
|
|
|
733
737
|
requestBody.parallel_tool_calls = true;
|
|
734
738
|
}
|
|
735
739
|
|
|
736
|
-
const
|
|
740
|
+
const fetcher = isOpenRouter ? createOpenRouterFetch({ apiKey: auth.apiKey }) : fetch;
|
|
741
|
+
const response = await fetcher(resolveOpenAIResponsesUrl(model), {
|
|
737
742
|
method: "POST",
|
|
738
743
|
headers: requestHeaders,
|
|
739
744
|
body: JSON.stringify(requestBody),
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { RuntimeModelEndpoint } from "../config/ports";
|
|
2
|
-
|
|
3
|
-
export interface OpenRouterRequestOptions {
|
|
4
|
-
readonly headers?: HeadersInit;
|
|
5
|
-
readonly signal?: AbortSignal;
|
|
6
|
-
readonly fetch?: typeof fetch;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function openRouterRequestUrl(
|
|
10
|
-
endpoint: Pick<RuntimeModelEndpoint, "protocol" | "baseURL">,
|
|
11
|
-
): string {
|
|
12
|
-
const baseURL = endpoint.baseURL.replace(/\/+$/, "");
|
|
13
|
-
switch (endpoint.protocol) {
|
|
14
|
-
case "openrouter-messages":
|
|
15
|
-
return `${baseURL}/v1/messages`;
|
|
16
|
-
case "openrouter-chat":
|
|
17
|
-
return `${baseURL}/chat/completions`;
|
|
18
|
-
case "openrouter-responses":
|
|
19
|
-
return `${baseURL}/responses`;
|
|
20
|
-
default:
|
|
21
|
-
throw new Error(`Not an OpenRouter protocol: ${endpoint.protocol}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function requestOpenRouter(
|
|
26
|
-
endpoint: RuntimeModelEndpoint,
|
|
27
|
-
body: Readonly<Record<string, unknown>>,
|
|
28
|
-
options: OpenRouterRequestOptions = {},
|
|
29
|
-
): Promise<Response> {
|
|
30
|
-
const url = openRouterRequestUrl(endpoint);
|
|
31
|
-
const headers = new Headers(endpoint.headers);
|
|
32
|
-
new Headers(options.headers).forEach((value, name) => headers.set(name, value));
|
|
33
|
-
headers.set("content-type", "application/json");
|
|
34
|
-
headers.set("authorization", `Bearer ${endpoint.apiKey}`);
|
|
35
|
-
return (options.fetch ?? globalThis.fetch)(url, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers,
|
|
38
|
-
body: JSON.stringify(body),
|
|
39
|
-
...(options.signal ? { signal: options.signal } : {}),
|
|
40
|
-
});
|
|
41
|
-
}
|