nanocodex 0.6.4 → 0.6.6
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/README.md +37 -11
- package/browser/Agent.d.mts +1 -1
- package/browser/ElevenLabsPlayback.mjs +119 -0
- package/browser/InlineAgent.mjs +22 -17
- package/browser/Transport.d.mts +2 -0
- package/browser/Transport.mjs +3 -2
- package/browser/Voice.d.mts +4 -0
- package/browser/Voice.mjs +38 -6
- package/browser/VoiceSession.mjs +37 -7
- package/browser/host.d.mts +20 -4
- package/browser/host.mjs +156 -18
- package/cloud/types.d.mts +1 -0
- package/cloudflare/Agent.d.mts +14 -9
- package/cloudflare/Agent.mjs +354 -96
- package/cloudflare/agent-settings.d.mts +15 -0
- package/cloudflare/agent-settings.mjs +135 -0
- package/cloudflare/chat-reasoning.mjs +20 -0
- package/cloudflare/durable-placement.d.mts +12 -0
- package/cloudflare/durable-placement.mjs +82 -0
- package/cloudflare/egress.d.mts +31 -0
- package/cloudflare/gateway-binding-responses.mjs +84 -0
- package/cloudflare/gateway-responses.d.mts +48 -0
- package/cloudflare/gateway-responses.mjs +191 -0
- package/cloudflare/index.d.mts +6 -0
- package/cloudflare/index.mjs +3 -0
- package/cloudflare/managed-auth.d.mts +15 -0
- package/cloudflare/managed-auth.mjs +119 -0
- package/cloudflare/managed-live.d.mts +7 -0
- package/cloudflare/managed-live.mjs +56 -0
- package/cloudflare/prepared-connection.mjs +49 -0
- package/cloudflare/provider-stream.mjs +353 -0
- package/cloudflare/rpc.d.mts +9 -0
- package/cloudflare/rpc.mjs +14 -0
- package/cloudflare/tool-alias-diagnostic.mjs +21 -0
- package/cloudflare/tracing.d.mts +16 -0
- package/cloudflare/tracing.mjs +47 -0
- package/cloudflare/workers-ai-responses.d.mts +28 -0
- package/cloudflare/workers-ai-responses.mjs +399 -0
- package/host/Agent.d.mts +1 -1
- package/index.d.mts +2 -0
- package/internal.mjs +53 -14
- package/managed/Agent.d.mts +25 -69
- package/managed/Agent.mjs +82 -196
- package/managed/README.md +9 -38
- package/managed/Voice.mjs +5 -2
- package/managed/index.d.mts +0 -3
- package/managed/internal.mjs +2 -1
- package/node/Agent.d.mts +1 -1
- package/node/Agent.mjs +9 -3
- package/node/host.mjs +5 -0
- package/package.json +34 -2
- package/pkg-node/nanocodex.d.ts +22 -18
- package/pkg-node/nanocodex.js +118 -92
- package/pkg-node/nanocodex_bg.wasm.d.ts +6 -5
- package/pkg-web/.nanocodex-bindgen-stamp +7 -1
- package/pkg-web/nanocodex-build.json +1 -1
- package/pkg-web/nanocodex.d.ts +28 -23
- package/pkg-web/nanocodex.js +117 -91
- package/pkg-web/nanocodex_bg.js +117 -91
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +6 -5
- package/pkg-web/nanocodex_worker.js +1 -1
- package/runtime/before-compaction.mjs +49 -0
- package/runtime/code-evaluator.worker.mjs +2 -9
- package/runtime/managed-transport.mjs +10 -2
- package/runtime/mcp-runtime.mjs +20 -4
- package/runtime/quickjs-evaluator.mjs +11 -5
- package/runtime/response-controls.mjs +67 -4
- package/runtime/steer-receipt.mjs +13 -0
- package/runtime/subagent-routing.d.mts +38 -0
- package/runtime/subagent-routing.mjs +81 -0
- package/runtime/subagents.d.mts +1 -1
- package/runtime/tempo-provider.d.mts +1 -1
- package/runtime/tempo-provider.mjs +1 -1
- package/tools/environment.d.mts +17 -2
- package/tools/environment.mjs +30 -3
- package/tools/index.d.mts +2 -0
- package/types.d.mts +30 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export const AGENT_MODELS = [
|
|
2
|
+
"gpt-6.1-sol",
|
|
3
|
+
"gpt-6-luna",
|
|
4
|
+
"gpt-6-astra",
|
|
5
|
+
"@cf/zai-org/glm-5.3",
|
|
6
|
+
"kimi-k3",
|
|
7
|
+
"mimo-v2.6-pro",
|
|
8
|
+
];
|
|
9
|
+
export const AGENT_THINKING = [
|
|
10
|
+
"none",
|
|
11
|
+
"low",
|
|
12
|
+
"medium",
|
|
13
|
+
"high",
|
|
14
|
+
"xhigh",
|
|
15
|
+
"max",
|
|
16
|
+
];
|
|
17
|
+
export const AGENT_REASONING_MODES = ["standard", "pro"];
|
|
18
|
+
export const DEFAULT_AGENT_SETTINGS = Object.freeze({
|
|
19
|
+
model: "gpt-6-astra",
|
|
20
|
+
thinking: "low",
|
|
21
|
+
reasoning_mode: "standard",
|
|
22
|
+
fast_mode: false,
|
|
23
|
+
});
|
|
24
|
+
const QUERY_KEYS = new Set([
|
|
25
|
+
"model",
|
|
26
|
+
"thinking",
|
|
27
|
+
"reasoning_mode",
|
|
28
|
+
"fast_mode",
|
|
29
|
+
]);
|
|
30
|
+
export function parseAgentSettingsQuery(search) {
|
|
31
|
+
for (const key of search.keys()) {
|
|
32
|
+
if (!QUERY_KEYS.has(key)
|
|
33
|
+
|| search.getAll(key).length !== 1) {
|
|
34
|
+
throw new TypeError("invalid agent settings query");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const model = search.get("model") ?? DEFAULT_AGENT_SETTINGS.model;
|
|
38
|
+
const thinking = search.get("thinking") ?? DEFAULT_AGENT_SETTINGS.thinking;
|
|
39
|
+
const reasoningMode = search.get("reasoning_mode")
|
|
40
|
+
?? DEFAULT_AGENT_SETTINGS.reasoning_mode;
|
|
41
|
+
const encodedFastMode = search.get("fast_mode");
|
|
42
|
+
if (!isAgentModel(model)
|
|
43
|
+
|| !isAgentThinking(thinking)
|
|
44
|
+
|| !isAgentReasoningMode(reasoningMode)
|
|
45
|
+
|| (encodedFastMode !== null
|
|
46
|
+
&& encodedFastMode !== "true"
|
|
47
|
+
&& encodedFastMode !== "false")) {
|
|
48
|
+
throw new TypeError("invalid agent settings query");
|
|
49
|
+
}
|
|
50
|
+
return validateAgentSettings({
|
|
51
|
+
model,
|
|
52
|
+
thinking,
|
|
53
|
+
reasoning_mode: reasoningMode,
|
|
54
|
+
fast_mode: encodedFastMode === null
|
|
55
|
+
? DEFAULT_AGENT_SETTINGS.fast_mode
|
|
56
|
+
: encodedFastMode === "true",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function agentSettingsQuery(settings) {
|
|
60
|
+
return new URLSearchParams({
|
|
61
|
+
model: settings.model,
|
|
62
|
+
thinking: settings.thinking,
|
|
63
|
+
reasoning_mode: settings.reasoning_mode,
|
|
64
|
+
fast_mode: String(settings.fast_mode),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export function parseAgentSettingsPatch(value) {
|
|
68
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
69
|
+
throw new TypeError("agent settings must be a JSON object");
|
|
70
|
+
}
|
|
71
|
+
const input = value;
|
|
72
|
+
const keys = Object.keys(input);
|
|
73
|
+
if (keys.length === 0 || keys.some((key) => !QUERY_KEYS.has(key))) {
|
|
74
|
+
throw new TypeError("agent settings contain unsupported fields");
|
|
75
|
+
}
|
|
76
|
+
if (Object.hasOwn(input, "model") && !isAgentModel(input.model)) {
|
|
77
|
+
throw new TypeError("invalid agent model");
|
|
78
|
+
}
|
|
79
|
+
if (Object.hasOwn(input, "thinking") && !isAgentThinking(input.thinking)) {
|
|
80
|
+
throw new TypeError("invalid agent thinking");
|
|
81
|
+
}
|
|
82
|
+
if (Object.hasOwn(input, "reasoning_mode") && !isAgentReasoningMode(input.reasoning_mode)) {
|
|
83
|
+
throw new TypeError("invalid agent reasoning mode");
|
|
84
|
+
}
|
|
85
|
+
if (Object.hasOwn(input, "fast_mode") && typeof input.fast_mode !== "boolean") {
|
|
86
|
+
throw new TypeError("invalid agent fast mode");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
...(Object.hasOwn(input, "model") ? { model: input.model } : {}),
|
|
90
|
+
...(Object.hasOwn(input, "thinking")
|
|
91
|
+
? { thinking: input.thinking }
|
|
92
|
+
: {}),
|
|
93
|
+
...(Object.hasOwn(input, "reasoning_mode")
|
|
94
|
+
? { reasoning_mode: input.reasoning_mode }
|
|
95
|
+
: {}),
|
|
96
|
+
...(Object.hasOwn(input, "fast_mode") ? { fast_mode: input.fast_mode } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export function parseCompleteAgentSettings(value) {
|
|
100
|
+
const settings = parseAgentSettingsPatch(value);
|
|
101
|
+
if (Object.keys(settings).length !== 4) {
|
|
102
|
+
throw new TypeError("agent settings must contain all four fields");
|
|
103
|
+
}
|
|
104
|
+
return validateAgentSettings(settings);
|
|
105
|
+
}
|
|
106
|
+
export function validateAgentSettings(settings) {
|
|
107
|
+
if (["@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"].includes(settings.model)
|
|
108
|
+
&& (!(settings.model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(settings.thinking) || settings.reasoning_mode !== "standard" || settings.fast_mode)) {
|
|
109
|
+
throw new TypeError("Gateway model requires a supported effort, standard mode, and no fast mode");
|
|
110
|
+
}
|
|
111
|
+
if (["gpt-6-astra", "gpt-6.1-sol"].includes(settings.model) && settings.thinking === "none") {
|
|
112
|
+
throw new TypeError("GPT-6 Astra and GPT-6.1 Sol require low, medium, high, xhigh, or max thinking");
|
|
113
|
+
}
|
|
114
|
+
if (settings.model === "gpt-6-astra" && settings.reasoning_mode === "pro") {
|
|
115
|
+
throw new TypeError("GPT-6 Astra does not support pro reasoning mode");
|
|
116
|
+
}
|
|
117
|
+
return settings;
|
|
118
|
+
}
|
|
119
|
+
/** Public admission cannot select the OSS model without a committed thread route. */
|
|
120
|
+
export function validateAgentAdmissionSettings(settings) {
|
|
121
|
+
if (["@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"].includes(settings.model)) {
|
|
122
|
+
throw new TypeError("Gateway model is available only through model_routing; omit explicit model settings");
|
|
123
|
+
}
|
|
124
|
+
return validateAgentSettings(settings);
|
|
125
|
+
}
|
|
126
|
+
export function isAgentModel(value) {
|
|
127
|
+
return typeof value === "string" && AGENT_MODELS.includes(value);
|
|
128
|
+
}
|
|
129
|
+
export function isAgentThinking(value) {
|
|
130
|
+
return typeof value === "string" && AGENT_THINKING.includes(value);
|
|
131
|
+
}
|
|
132
|
+
export function isAgentReasoningMode(value) {
|
|
133
|
+
return typeof value === "string"
|
|
134
|
+
&& AGENT_REASONING_MODES.includes(value);
|
|
135
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// OpenRouter can mirror visible reasoning in both its legacy string fields and
|
|
2
|
+
// reasoning_details. Project one representation per chunk; retain the original
|
|
3
|
+
// details separately for tool-history replay, including opaque signatures/data.
|
|
4
|
+
export function chatReasoningText(message) {
|
|
5
|
+
const text = message.reasoning_content ?? message.reasoning;
|
|
6
|
+
if (text != null && typeof text !== "string") throw new Error("Responses: invalid reasoning text");
|
|
7
|
+
const details = message.reasoning_details;
|
|
8
|
+
if (details != null && (!Array.isArray(details)
|
|
9
|
+
|| details.some(detail => !detail || typeof detail !== "object" || Array.isArray(detail)))) {
|
|
10
|
+
throw new Error("Responses: invalid reasoning details");
|
|
11
|
+
}
|
|
12
|
+
let visible = "";
|
|
13
|
+
for (const detail of details ?? []) {
|
|
14
|
+
const value = detail.type === "reasoning.text" ? detail.text
|
|
15
|
+
: detail.type === "reasoning.summary" ? detail.summary : undefined;
|
|
16
|
+
if (value != null && typeof value !== "string") throw new Error("Responses: invalid reasoning detail text");
|
|
17
|
+
visible += value ?? "";
|
|
18
|
+
}
|
|
19
|
+
return text || visible;
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Trusted metadata retained from the initial user ingress, never a public header. */
|
|
2
|
+
export interface IngressPlacement { trustedClientIngressColo?: string | null }
|
|
3
|
+
export type PlacementRegion = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc";
|
|
4
|
+
export const TRUSTED_INGRESS_HEADER: "x-nanocodex-placement-colo";
|
|
5
|
+
export function ingressColo(value: unknown): string | null;
|
|
6
|
+
export function placementRegion(value: unknown): PlacementRegion | undefined;
|
|
7
|
+
/** Best-effort first-use hint. Does not rename or move an existing object. */
|
|
8
|
+
export function durablePlacementOptions(colo: unknown): { locationHint: PlacementRegion } | undefined;
|
|
9
|
+
export function placementHeaders(headers: HeadersInit | undefined, colo: unknown): Headers;
|
|
10
|
+
type PlacementBinding = { fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> };
|
|
11
|
+
/** Scope only a request's environment; keep retained DO environments unchanged. */
|
|
12
|
+
export function withIngressPlacement<T extends { NANOCODEX?: PlacementBinding }>(env: T, colo: unknown): T & IngressPlacement;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Cloudflare hints affect only first use and never change object identity.
|
|
2
|
+
* Hints are best effort, not jurisdiction or execution-colo assertions.
|
|
3
|
+
* https://developers.cloudflare.com/durable-objects/reference/data-location/ */
|
|
4
|
+
const TRUSTED_INGRESS_HEADER = "x-nanocodex-placement-colo";
|
|
5
|
+
const COLOS = {
|
|
6
|
+
SFO: "wnam",
|
|
7
|
+
SJC: "wnam",
|
|
8
|
+
LAX: "wnam",
|
|
9
|
+
SEA: "wnam",
|
|
10
|
+
PDX: "wnam",
|
|
11
|
+
PHX: "wnam",
|
|
12
|
+
DEN: "wnam",
|
|
13
|
+
LAS: "wnam",
|
|
14
|
+
SLC: "wnam",
|
|
15
|
+
IAD: "enam",
|
|
16
|
+
EWR: "enam",
|
|
17
|
+
BOS: "enam",
|
|
18
|
+
ATL: "enam",
|
|
19
|
+
ORD: "enam",
|
|
20
|
+
MIA: "enam",
|
|
21
|
+
LHR: "weur",
|
|
22
|
+
CDG: "weur",
|
|
23
|
+
FRA: "weur",
|
|
24
|
+
AMS: "weur",
|
|
25
|
+
MXP: "weur",
|
|
26
|
+
MAD: "weur",
|
|
27
|
+
DUB: "weur",
|
|
28
|
+
ZRH: "weur",
|
|
29
|
+
WAW: "eeur",
|
|
30
|
+
OTP: "eeur",
|
|
31
|
+
ATH: "eeur",
|
|
32
|
+
SIN: "apac",
|
|
33
|
+
NRT: "apac",
|
|
34
|
+
HKG: "apac",
|
|
35
|
+
SYD: "oc",
|
|
36
|
+
MEL: "oc",
|
|
37
|
+
AKL: "oc",
|
|
38
|
+
GRU: "sam",
|
|
39
|
+
SCL: "sam",
|
|
40
|
+
EZE: "sam"
|
|
41
|
+
};
|
|
42
|
+
function ingressColo(value) {
|
|
43
|
+
return typeof value === "string" && /^[A-Z]{3}$/.test(value) ? value : null;
|
|
44
|
+
}
|
|
45
|
+
function placementRegion(value) {
|
|
46
|
+
const colo = ingressColo(value);
|
|
47
|
+
return colo === null ? void 0 : COLOS[colo];
|
|
48
|
+
}
|
|
49
|
+
function durablePlacementOptions(colo) {
|
|
50
|
+
const locationHint = placementRegion(colo);
|
|
51
|
+
return locationHint ? { locationHint } : void 0;
|
|
52
|
+
}
|
|
53
|
+
/** Replace any caller assertion with platform or retained Session metadata. */
|
|
54
|
+
function placementHeaders(headers, colo) {
|
|
55
|
+
const result = new Headers(headers);
|
|
56
|
+
result.delete(TRUSTED_INGRESS_HEADER);
|
|
57
|
+
const trusted = ingressColo(colo);
|
|
58
|
+
if (trusted) result.set(TRUSTED_INGRESS_HEADER, trusted);
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/** Scope a request-local env only; never replace a retained DO env or its
|
|
62
|
+
* discovery-cache/model-transport binding identities. */
|
|
63
|
+
function withIngressPlacement(env, colo) {
|
|
64
|
+
const trustedClientIngressColo = ingressColo(colo);
|
|
65
|
+
const binding = env.NANOCODEX;
|
|
66
|
+
return { ...env, trustedClientIngressColo, ...binding ? {
|
|
67
|
+
NANOCODEX: {
|
|
68
|
+
fetch(input, init) {
|
|
69
|
+
const request = new Request(input, init);
|
|
70
|
+
return binding.fetch(new Request(request, { headers: placementHeaders(request.headers, trustedClientIngressColo) }));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} : {} };
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
TRUSTED_INGRESS_HEADER,
|
|
77
|
+
durablePlacementOptions,
|
|
78
|
+
ingressColo,
|
|
79
|
+
placementHeaders,
|
|
80
|
+
placementRegion,
|
|
81
|
+
withIngressPlacement
|
|
82
|
+
};
|
package/cloudflare/egress.d.mts
CHANGED
|
@@ -9,6 +9,37 @@ export type CloudflareEgressBinding = Readonly<{
|
|
|
9
9
|
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
10
10
|
}>;
|
|
11
11
|
|
|
12
|
+
/** Plain private discovery results. Consumers must validate and project current authority. */
|
|
13
|
+
export type CloudflareAccountCatalogResult = Readonly<{ status: number; catalog: unknown }>;
|
|
14
|
+
export type CloudflareAccountVaultResult = Readonly<{ status: number; vault: unknown }>;
|
|
15
|
+
|
|
16
|
+
export type CloudflareAccountMetadataComponent = "catalog" | "vault";
|
|
17
|
+
export type CloudflareAccountDiscoveryOptions = Readonly<{
|
|
18
|
+
/** Current caller authority partition, at most 4096 UTF-8 bytes; not authorization. */
|
|
19
|
+
authorityKey: string;
|
|
20
|
+
/** Always read the backend. Does not globally invalidate other isolates. */
|
|
21
|
+
reload?: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
/** Original backend-read expiry. Consumers must not start another 15-minute TTL. */
|
|
24
|
+
export type CloudflareAccountDiscoveryResult = Readonly<{
|
|
25
|
+
schema: 1;
|
|
26
|
+
status: number;
|
|
27
|
+
data: unknown;
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
/** Metadata only; this surface never returns provider credentials or Vault secrets. */
|
|
32
|
+
export type CloudflareAccountMetadataBinding = CloudflareEgressBinding & Readonly<{
|
|
33
|
+
readAccountCatalog?: (userId: string) => Promise<CloudflareAccountCatalogResult>;
|
|
34
|
+
readAccountVault?: (userId: string) => Promise<CloudflareAccountVaultResult>;
|
|
35
|
+
/** Opt-in discovery cache. Live methods above and HTTP retain their existing defaults.
|
|
36
|
+
* Other isolates may see older metadata within its original 15-minute lifetime.
|
|
37
|
+
* Callers must project current authority; these snapshots never authorize execution.
|
|
38
|
+
*/
|
|
39
|
+
readAccountDiscovery?: (userId: string, component: CloudflareAccountMetadataComponent,
|
|
40
|
+
options: CloudflareAccountDiscoveryOptions) => Promise<CloudflareAccountDiscoveryResult>;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
12
43
|
export type CloudflareEgressOptions = Readonly<{
|
|
13
44
|
/** The managed Worker's private EGRESS Service Binding. */
|
|
14
45
|
binding: CloudflareEgressBinding;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The portable adapter owns tool aliases and full-history validation. This
|
|
2
|
+
// bridge only changes wire formats: Cloudflare's OpenAI binding takes Responses,
|
|
3
|
+
// including for models that do not offer Chat Completions.
|
|
4
|
+
const fail = () => { throw new Error("Gateway Responses: invalid binding response"); };
|
|
5
|
+
|
|
6
|
+
export function toBindingResponsesInput(chat, effort) {
|
|
7
|
+
const input = [];
|
|
8
|
+
for (const message of chat.messages) {
|
|
9
|
+
if (message.role === "tool") {
|
|
10
|
+
input.push({ type: "function_call_output", call_id: message.tool_call_id, output: message.content });
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
// Portable reasoning is plaintext history, never a provider-issued opaque
|
|
14
|
+
// reasoning item or encrypted state. Replay it as assistant text.
|
|
15
|
+
if (message.reasoning_content) input.push({ role: "assistant", content: message.reasoning_content });
|
|
16
|
+
if (message.content != null) input.push({ role: message.role, content: Array.isArray(message.content)
|
|
17
|
+
? message.content.map(part => part.type === "image_url"
|
|
18
|
+
? { type: "input_image", image_url: part.image_url.url, ...(part.image_url.detail ? { detail: part.image_url.detail } : {}) }
|
|
19
|
+
: { type: message.role === "assistant" ? "output_text" : "input_text", text: part.text })
|
|
20
|
+
: message.content });
|
|
21
|
+
for (const call of message.tool_calls ?? []) input.push({ type: "function_call", call_id: call.id,
|
|
22
|
+
name: call.function.name, arguments: call.function.arguments });
|
|
23
|
+
}
|
|
24
|
+
const payload = { input, stream: chat.stream === true, store: false, reasoning: { effort } };
|
|
25
|
+
if (chat.tools) payload.tools = chat.tools.map(tool => ({ type: "function", ...tool.function,
|
|
26
|
+
// Responses normalizes omitted strict to true; preserve explicit strict
|
|
27
|
+
// functions while leaving legacy optional/loose schemas non-strict.
|
|
28
|
+
strict: tool.function.strict === true }));
|
|
29
|
+
if (chat.tool_choice !== undefined) payload.tool_choice = typeof chat.tool_choice === "string"
|
|
30
|
+
? chat.tool_choice : { type: "function", name: chat.tool_choice.function.name };
|
|
31
|
+
if (chat.max_completion_tokens !== undefined) payload.max_output_tokens = chat.max_completion_tokens;
|
|
32
|
+
for (const field of ["temperature", "top_p", "parallel_tool_calls"]) {
|
|
33
|
+
if (chat[field] !== undefined) payload[field] = chat[field];
|
|
34
|
+
}
|
|
35
|
+
return payload;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function fromBindingResponsesResult(result) {
|
|
39
|
+
if (!result || typeof result !== "object" || result.error || result.object !== "response"
|
|
40
|
+
|| !["completed", "incomplete"].includes(result.status) || !Array.isArray(result.output)) fail();
|
|
41
|
+
let finish_reason = "stop";
|
|
42
|
+
if (result.status === "incomplete") {
|
|
43
|
+
const reason = result.incomplete_details?.reason;
|
|
44
|
+
if (!["max_output_tokens", "content_filter"].includes(reason)) fail();
|
|
45
|
+
finish_reason = reason === "max_output_tokens" ? "length" : "content_filter";
|
|
46
|
+
}
|
|
47
|
+
const texts = [], reasoning = [], tool_calls = [];
|
|
48
|
+
for (const item of result.output) {
|
|
49
|
+
if (!item || typeof item !== "object") fail();
|
|
50
|
+
if (item.status !== undefined && item.status !== "completed"
|
|
51
|
+
&& !(result.status === "incomplete" && item.status === "incomplete")) fail();
|
|
52
|
+
if (item.type === "message") {
|
|
53
|
+
if (item.role !== "assistant" || !Array.isArray(item.content)) fail();
|
|
54
|
+
for (const part of item.content) {
|
|
55
|
+
if (part?.type !== "output_text" || typeof part.text !== "string") fail();
|
|
56
|
+
texts.push(part.text);
|
|
57
|
+
}
|
|
58
|
+
} else if (item.type === "reasoning") {
|
|
59
|
+
for (const [field, type] of [["summary", "summary_text"], ["content", "reasoning_text"]]) {
|
|
60
|
+
if (item[field] !== undefined && !Array.isArray(item[field])) fail();
|
|
61
|
+
for (const part of item[field] ?? []) {
|
|
62
|
+
if (part?.type !== type || typeof part.text !== "string") fail();
|
|
63
|
+
reasoning.push(part.text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// encrypted_content intentionally stays upstream: full history is portable.
|
|
67
|
+
} else if (item.type === "function_call") {
|
|
68
|
+
if (result.status !== "completed" || typeof item.call_id !== "string" || !item.call_id
|
|
69
|
+
|| typeof item.name !== "string" || typeof item.arguments !== "string") fail();
|
|
70
|
+
tool_calls.push({ id: item.call_id, type: "function", function: { name: item.name, arguments: item.arguments } });
|
|
71
|
+
} else fail(); // Never accept provider-hosted tools or unsupported modalities.
|
|
72
|
+
}
|
|
73
|
+
if (tool_calls.length) finish_reason = "tool_calls";
|
|
74
|
+
const message = { content: texts.join("\n"), reasoning_content: reasoning.join("\n"), tool_calls };
|
|
75
|
+
let usage;
|
|
76
|
+
if (result.usage != null) {
|
|
77
|
+
const count = value => { if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) fail(); return value; };
|
|
78
|
+
usage = { prompt_tokens: count(result.usage.input_tokens), completion_tokens: count(result.usage.output_tokens),
|
|
79
|
+
total_tokens: count(result.usage.total_tokens),
|
|
80
|
+
prompt_tokens_details: { cached_tokens: count(result.usage.input_tokens_details?.cached_tokens) },
|
|
81
|
+
completion_tokens_details: { reasoning_tokens: count(result.usage.output_tokens_details?.reasoning_tokens) } };
|
|
82
|
+
}
|
|
83
|
+
return { choices: [{ message, finish_reason }], ...(usage ? { usage } : {}) };
|
|
84
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ResponsesCanonicalModel, WorkersAiResponsesTransport } from "./workers-ai-responses.mjs";
|
|
2
|
+
/** Content-free, attempt-level telemetry. No requests, URLs, errors or response bodies are exposed. */
|
|
3
|
+
export type GatewayRequestOutcome = "success" | "http_error" | "network_error" | "protocol_error" | "timeout" | "cancelled";
|
|
4
|
+
export type GatewayRequestObserver = Readonly<{
|
|
5
|
+
/** HTTP only; binding attempts leave the status unset. */
|
|
6
|
+
headers(status: number): void;
|
|
7
|
+
/** First emitted nonempty text or validated tool output; excludes reasoning and HTTP headers. */
|
|
8
|
+
firstToken?(): void;
|
|
9
|
+
/** Success follows body consumption and protocol validation; HTTP headers are not TTFT. */
|
|
10
|
+
finish(outcome: GatewayRequestOutcome): void | boolean | Promise<void | boolean>;
|
|
11
|
+
}>;
|
|
12
|
+
type GatewayResponsesCommonOptions = Readonly<{
|
|
13
|
+
reasoningEffort: "none" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
14
|
+
/** Called once immediately before each outbound attempt. Failures cannot break generation. */
|
|
15
|
+
onRequest?: () => GatewayRequestObserver;
|
|
16
|
+
}>;
|
|
17
|
+
export type GatewayResponsesOptions = GatewayResponsesCommonOptions & (Readonly<{
|
|
18
|
+
provider: "openrouter" | "vercel";
|
|
19
|
+
model: ResponsesCanonicalModel;
|
|
20
|
+
/** Server-side credential. Never include in browser state or persisted routing metadata. */
|
|
21
|
+
apiKey: string;
|
|
22
|
+
/** Injectable HTTP transport, defaulting to global fetch. */
|
|
23
|
+
fetch?: typeof globalThis.fetch;
|
|
24
|
+
ai?: never;
|
|
25
|
+
accountId?: never;
|
|
26
|
+
}> | Readonly<{
|
|
27
|
+
provider: "cloudflare";
|
|
28
|
+
model: Extract<ResponsesCanonicalModel, `gpt-${string}`>;
|
|
29
|
+
/** Native Responses binding; credentials stay with Cloudflare. Cancellation stops waiting, not inference. */
|
|
30
|
+
ai: { run(model: `openai/${Extract<ResponsesCanonicalModel, `gpt-${string}`>}`, input: Record<string, unknown>): Promise<unknown> };
|
|
31
|
+
apiKey?: never;
|
|
32
|
+
fetch?: never;
|
|
33
|
+
accountId?: never;
|
|
34
|
+
}> | Readonly<{
|
|
35
|
+
provider: "cloudflare";
|
|
36
|
+
model: Extract<ResponsesCanonicalModel, `gpt-${string}`>;
|
|
37
|
+
/** Account-scoped native Responses REST; no provider API key or connector access. */
|
|
38
|
+
accountId: string;
|
|
39
|
+
/** Deployment-owned Cloudflare API token with inference access. */
|
|
40
|
+
apiKey: string;
|
|
41
|
+
fetch?: typeof globalThis.fetch;
|
|
42
|
+
ai?: never;
|
|
43
|
+
}>);
|
|
44
|
+
/** Incremental (stream:true) or buffered Responses SSE with complete replay and pinned model/effort.
|
|
45
|
+
* Text/reasoning stream incrementally; tools are emitted only after terminal validation.
|
|
46
|
+
* x-nanocodex-inference-buffering reports streaming or buffered binding fallback.
|
|
47
|
+
*/
|
|
48
|
+
export function createGatewayResponses(options: GatewayResponsesOptions): WorkersAiResponsesTransport & Readonly<{ stateless: true }>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { providerStream } from "./provider-stream.mjs";
|
|
2
|
+
import { createWorkersAiResponses } from "./workers-ai-responses.mjs";
|
|
3
|
+
import { toBindingResponsesInput, fromBindingResponsesResult } from "./gateway-binding-responses.mjs";
|
|
4
|
+
|
|
5
|
+
const ENDPOINTS = Object.freeze({
|
|
6
|
+
openrouter: "https://openrouter.ai/api/v1/chat/completions",
|
|
7
|
+
vercel: "https://ai-gateway.vercel.sh/v1/chat/completions",
|
|
8
|
+
});
|
|
9
|
+
const MODELS = ["@cf/zai-org/glm-5.3", "gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna", "kimi-k3", "mimo-v2.6-pro"];
|
|
10
|
+
const fail = message => { throw new Error(`Gateway Responses: ${message}`); };
|
|
11
|
+
|
|
12
|
+
/** Server-side, full-history transport; no WebSocket or opaque compaction. */
|
|
13
|
+
export function createGatewayResponses(options) {
|
|
14
|
+
const { provider, model, reasoningEffort, apiKey, fetch: fetchImpl = globalThis.fetch } = options;
|
|
15
|
+
if (provider !== "cloudflare" && !Object.hasOwn(ENDPOINTS, provider)) fail("unsupported provider");
|
|
16
|
+
if (!MODELS.includes(model)) fail("unsupported canonical model");
|
|
17
|
+
if (!(model === "gpt-6-luna" ? ["none", "low", "medium", "high", "xhigh", "max"] : model === "gpt-6.1-sol" ? ["low", "medium", "high", "xhigh", "max"] : model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(reasoningEffort)) fail("unsupported reasoning effort");
|
|
18
|
+
const cloudflareHttp = provider === "cloudflare" && (options.accountId !== undefined || apiKey !== undefined);
|
|
19
|
+
if (provider === "cloudflare") {
|
|
20
|
+
if (!model.startsWith("gpt-")) fail("Cloudflare gateway requires an OpenAI canonical model");
|
|
21
|
+
if (cloudflareHttp) {
|
|
22
|
+
if (options.ai !== undefined) fail("choose one Cloudflare transport");
|
|
23
|
+
if (typeof options.accountId !== "string" || !/^[a-f0-9]{32}$/i.test(options.accountId)) fail("a valid Cloudflare account ID is required");
|
|
24
|
+
} else if (typeof options.ai?.run !== "function") fail("an AI binding is required");
|
|
25
|
+
}
|
|
26
|
+
if (provider !== "cloudflare" || cloudflareHttp) {
|
|
27
|
+
if (typeof apiKey !== "string" || !apiKey.trim() || /[\r\n]/.test(apiKey)
|
|
28
|
+
|| (cloudflareHttp && !/^[\x21-\x7e]+$/.test(apiKey))) fail("a server-side API key is required");
|
|
29
|
+
if (typeof fetchImpl !== "function") fail("fetch is required");
|
|
30
|
+
}
|
|
31
|
+
const endpoint = cloudflareHttp
|
|
32
|
+
? `https://api.cloudflare.com/client/v4/accounts/${options.accountId}/ai/v1/responses` : ENDPOINTS[provider];
|
|
33
|
+
const gatewayModel = model === MODELS[0]
|
|
34
|
+
? (provider === "openrouter" ? "z-ai/glm-5.3" : "zai/glm-5.3") : model === "kimi-k3" ? "moonshotai/kimi-k3" : model === "mimo-v2.6-pro" ? "xiaomi/mimo-v2.6-pro" : `openai/${model}`;
|
|
35
|
+
const apiBaseUrl = `https://${provider}-responses.invalid/v1`;
|
|
36
|
+
const adapter = (signal, attempt, reasoningMode) => createWorkersAiResponses({
|
|
37
|
+
async run(_model, input) {
|
|
38
|
+
signal?.throwIfAborted();
|
|
39
|
+
if (provider !== "cloudflare" && input.tools?.length && (model === "gpt-6.1-sol" || model === "gpt-6-luna" && reasoningEffort !== "none")) {
|
|
40
|
+
fail("pinned Chat model/effort cannot call tools; use Responses");
|
|
41
|
+
}
|
|
42
|
+
if (input.reasoning_effort !== undefined && input.reasoning_effort !== reasoningEffort) fail("reasoning override does not match pinned effort");
|
|
43
|
+
if (provider === "cloudflare" && !cloudflareHttp) {
|
|
44
|
+
const payload = toBindingResponsesInput(input, reasoningEffort);
|
|
45
|
+
if (reasoningMode !== undefined) payload.reasoning.mode = reasoningMode;
|
|
46
|
+
attempt.outcome = "network_error";
|
|
47
|
+
try { attempt.observer = options.onRequest?.(); } catch { /* telemetry is best effort */ }
|
|
48
|
+
let value;
|
|
49
|
+
try { value = await options.ai.run(gatewayModel, payload); }
|
|
50
|
+
catch {
|
|
51
|
+
signal?.throwIfAborted();
|
|
52
|
+
fail("provider request failed");
|
|
53
|
+
}
|
|
54
|
+
if (signal?.aborted) {
|
|
55
|
+
if (value instanceof ReadableStream) void value.cancel().catch(() => {});
|
|
56
|
+
signal.throwIfAborted();
|
|
57
|
+
}
|
|
58
|
+
// A binding has no observable HTTP headers/status. Protocol validation
|
|
59
|
+
// and the existing portable adapter must both succeed before finish().
|
|
60
|
+
attempt.outcome = "protocol_error";
|
|
61
|
+
if (input.stream && value instanceof ReadableStream) {
|
|
62
|
+
attempt.streaming = true;
|
|
63
|
+
return providerStream(value, "responses", attempt.hooks);
|
|
64
|
+
}
|
|
65
|
+
return fromBindingResponsesResult(value);
|
|
66
|
+
}
|
|
67
|
+
// Vercel documents reasoning_effort as the Chat Completions alias:
|
|
68
|
+
// https://vercel.com/docs/ai-gateway/sdks-and-apis/openai-chat-completions/reasoning
|
|
69
|
+
const payload = cloudflareHttp
|
|
70
|
+
? { ...toBindingResponsesInput(input, reasoningEffort), model: gatewayModel }
|
|
71
|
+
: { ...input, model: gatewayModel, reasoning_effort: reasoningEffort };
|
|
72
|
+
if (cloudflareHttp && reasoningMode !== undefined) payload.reasoning.mode = reasoningMode;
|
|
73
|
+
if (["kimi-k3", "mimo-v2.6-pro"].includes(model)) {
|
|
74
|
+
// Both providers document these models through the unified reasoning field.
|
|
75
|
+
delete payload.reasoning_effort;
|
|
76
|
+
payload.reasoning = { effort: reasoningEffort };
|
|
77
|
+
if (payload.max_completion_tokens !== undefined) {
|
|
78
|
+
payload.max_tokens = payload.max_completion_tokens;
|
|
79
|
+
delete payload.max_completion_tokens;
|
|
80
|
+
}
|
|
81
|
+
if (payload.parallel_tool_calls === false) delete payload.parallel_tool_calls;
|
|
82
|
+
}
|
|
83
|
+
if (provider === "openrouter") {
|
|
84
|
+
delete payload.reasoning_effort;
|
|
85
|
+
payload.reasoning = { effort: reasoningEffort };
|
|
86
|
+
payload.provider = { require_parameters: true };
|
|
87
|
+
// OpenRouter filters on parameter presence, including false. Its catalog
|
|
88
|
+
// omits parallel_tool_calls for otherwise tool-capable endpoints. The host
|
|
89
|
+
// scheduler controls concurrency for all validated returned calls.
|
|
90
|
+
if (input.parallel_tool_calls === false) delete payload.parallel_tool_calls;
|
|
91
|
+
}
|
|
92
|
+
let response;
|
|
93
|
+
if (provider === "openrouter" && model === "mimo-v2.6-pro") {
|
|
94
|
+
// MiMo endpoints currently support automatic tool selection only. Keep
|
|
95
|
+
// forced-choice semantics locally: restrict advertised tools, then the
|
|
96
|
+
// portable normalizer validates the completed call before dispatch.
|
|
97
|
+
if (payload.tool_choice === "none") {
|
|
98
|
+
delete payload.tools;
|
|
99
|
+
delete payload.tool_choice;
|
|
100
|
+
} else if (payload.tool_choice === "required" || typeof payload.tool_choice === "object") {
|
|
101
|
+
if (typeof payload.tool_choice === "object") payload.tools = payload.tools.filter(tool => tool.function.name === payload.tool_choice.function.name);
|
|
102
|
+
payload.tool_choice = "auto";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
attempt.outcome = "network_error";
|
|
106
|
+
try { attempt.observer = options.onRequest?.(); } catch { /* telemetry is best effort */ }
|
|
107
|
+
try {
|
|
108
|
+
response = await fetchImpl(endpoint, { method: "POST", redirect: "manual", signal,
|
|
109
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
110
|
+
body: JSON.stringify(payload) });
|
|
111
|
+
} catch {
|
|
112
|
+
signal?.throwIfAborted();
|
|
113
|
+
fail("provider request failed");
|
|
114
|
+
}
|
|
115
|
+
if (signal?.aborted) {
|
|
116
|
+
try { await response.body?.cancel(); } catch { /* best effort release */ }
|
|
117
|
+
signal.throwIfAborted();
|
|
118
|
+
}
|
|
119
|
+
try { attempt.observer?.headers(response.status); } catch { /* telemetry is best effort */ }
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
attempt.outcome = "http_error";
|
|
122
|
+
// Never parse, quote, or retain an upstream error body or status text.
|
|
123
|
+
try { await response.body?.cancel(); } catch { /* best effort release */ }
|
|
124
|
+
fail("provider rejected request");
|
|
125
|
+
}
|
|
126
|
+
attempt.outcome = "protocol_error";
|
|
127
|
+
if (input.stream) {
|
|
128
|
+
if (!response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream")) {
|
|
129
|
+
try { await response.body?.cancel(); } catch { /* best effort */ }
|
|
130
|
+
fail("invalid provider stream");
|
|
131
|
+
}
|
|
132
|
+
attempt.streaming = true;
|
|
133
|
+
return providerStream(response.body, cloudflareHttp ? "responses" : "chat", attempt.hooks);
|
|
134
|
+
}
|
|
135
|
+
let value;
|
|
136
|
+
try { value = await response.json(); }
|
|
137
|
+
catch (error) {
|
|
138
|
+
signal?.throwIfAborted();
|
|
139
|
+
attempt.outcome = error instanceof SyntaxError ? "protocol_error" : "network_error";
|
|
140
|
+
fail("invalid provider response");
|
|
141
|
+
}
|
|
142
|
+
return cloudflareHttp ? fromBindingResponsesResult(value) : value;
|
|
143
|
+
},
|
|
144
|
+
}, { model, apiBaseUrl });
|
|
145
|
+
return Object.freeze({ apiBaseUrl, stateless: true,
|
|
146
|
+
async createResponse(endpoint, sessionId, request) {
|
|
147
|
+
const attempt = {};
|
|
148
|
+
let deferred = false, finished = false;
|
|
149
|
+
const finish = async outcome => {
|
|
150
|
+
if (finished) return;
|
|
151
|
+
finished = true;
|
|
152
|
+
if (request.signal?.aborted) outcome = request.signal.reason?.name === "TimeoutError" ? "timeout" : "cancelled";
|
|
153
|
+
try { await attempt.observer?.finish(outcome); } catch { /* best effort */ }
|
|
154
|
+
};
|
|
155
|
+
attempt.hooks = {
|
|
156
|
+
firstToken() { try { attempt.observer?.firstToken?.(); } catch { /* best effort */ } },
|
|
157
|
+
finish,
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
// The portable Chat translator has no reasoning.mode field. Preserve it
|
|
161
|
+
// explicitly on Responses transports and reject unsupported pro requests.
|
|
162
|
+
const body = JSON.parse(request.body);
|
|
163
|
+
const supportsMode = ["gpt-6.1-sol", "gpt-6-luna"].includes(model);
|
|
164
|
+
const validateMode = value => {
|
|
165
|
+
if (value !== undefined && !["standard", "pro"].includes(value)) fail("unsupported reasoning mode");
|
|
166
|
+
if (value === "pro" && (provider !== "cloudflare" || !supportsMode)) fail("pro reasoning requires a supported Responses model");
|
|
167
|
+
return value;
|
|
168
|
+
};
|
|
169
|
+
let reasoningMode = validateMode(body.reasoning?.mode);
|
|
170
|
+
if (Array.isArray(body.input)) for (const item of body.input) {
|
|
171
|
+
if (item.type === "configuration_update") reasoningMode = validateMode(item.reasoning?.mode) ?? reasoningMode;
|
|
172
|
+
}
|
|
173
|
+
if (provider !== "cloudflare" || !supportsMode) reasoningMode = undefined;
|
|
174
|
+
const response = await adapter(request.signal, attempt, reasoningMode).createResponse(endpoint, sessionId, request);
|
|
175
|
+
attempt.outcome = "success";
|
|
176
|
+
deferred = attempt.streaming === true;
|
|
177
|
+
return response;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
request.signal?.throwIfAborted();
|
|
181
|
+
// Adapter validation must not echo untrusted request/provider fields either.
|
|
182
|
+
fail("request failed or is incompatible with the pinned model and effort");
|
|
183
|
+
} finally {
|
|
184
|
+
if (request.signal?.aborted) {
|
|
185
|
+
attempt.outcome = request.signal.reason?.name === "TimeoutError" ? "timeout" : "cancelled";
|
|
186
|
+
}
|
|
187
|
+
if (!deferred) await finish(attempt.outcome);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
package/cloudflare/index.d.mts
CHANGED
|
@@ -5,3 +5,9 @@ export type {
|
|
|
5
5
|
CloudflareEgressOptions,
|
|
6
6
|
CloudflareEgressTransportOptions,
|
|
7
7
|
} from "./egress.mjs";
|
|
8
|
+
export { createWorkersAiResponses } from "./workers-ai-responses.mjs";
|
|
9
|
+
export { createGatewayResponses } from "./gateway-responses.mjs";
|
|
10
|
+
export type { GatewayResponsesOptions } from "./gateway-responses.mjs";
|
|
11
|
+
export type { ResponsesCanonicalModel } from "./workers-ai-responses.mjs";
|
|
12
|
+
export { createSubagentRouting } from "../runtime/subagent-routing.mjs";
|
|
13
|
+
export type { ChildRoute, ChildRouteRequest, ChildRouteBinding, SubagentRouting } from "../runtime/subagent-routing.mjs";
|
package/cloudflare/index.mjs
CHANGED
|
@@ -4,3 +4,6 @@ import { bindAgent } from "./Agent.mjs";
|
|
|
4
4
|
/** Cloudflare Durable Object Agent backed by the package's compiled WASM. */
|
|
5
5
|
export const Agent = bindAgent(module);
|
|
6
6
|
export { cloudflareEgress } from "./egress.mjs";
|
|
7
|
+
export { createWorkersAiResponses } from "./workers-ai-responses.mjs";
|
|
8
|
+
export { createGatewayResponses } from "./gateway-responses.mjs";
|
|
9
|
+
export { createSubagentRouting } from "../runtime/subagent-routing.mjs";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type OrganizationCapability = "agents:read" | "agents:portability" | "agents:write" | "api_keys:read" | "api_keys:write" | "history:read" | "memory:read" | "memory:write" | "tools:use" | "organization:read" | "organization:write";
|
|
2
|
+
export type ApiKeyBase = Readonly<{ id: string; label: string; prefix: string; createdAt: number; digest: string; userId: string }>;
|
|
3
|
+
export type StoredApiKey = ApiKeyBase & Readonly<{ organizationId: string; teamId: string; role: "owner" | "writer" | "reader"; capabilities: readonly OrganizationCapability[]; authorizationEpoch: number }>;
|
|
4
|
+
export type ApiKeyPrincipal = Readonly<{ kind: "api_key"; userId: string; organizationId: string; teamId: string; role: StoredApiKey["role"]; subjectId: `api_key:${string}`; credentialId: string; authorizationEpoch: number; capabilities: readonly OrganizationCapability[] }>;
|
|
5
|
+
export type AdmissionPrincipal = Readonly<{ kind: "api_key" | "account_session" | "connect_grant" | "service"; userId: string; organizationId: string; teamId: string; authorizationEpoch: number; capabilities: readonly string[]; connectGrant?: Readonly<{ grantId: string; connectors: readonly string[]; connectorConnections?: unknown; mcpIds: readonly string[]; appToolCatalogDigest?: string; sandboxExecution?: true; outputCheckpoints?: true }> }>;
|
|
6
|
+
export const API_KEY: RegExp;
|
|
7
|
+
export function isUserId(value: unknown): value is string;
|
|
8
|
+
export function isOrganizationCapabilities(value: unknown): value is readonly OrganizationCapability[];
|
|
9
|
+
export function isApiKeyBase(value: unknown): value is ApiKeyBase;
|
|
10
|
+
export function isStoredApiKey(value: unknown): value is StoredApiKey;
|
|
11
|
+
export function apiKeyDigest(request: Request): Promise<string | undefined>;
|
|
12
|
+
/** Only a record returned by live authorized key resolution may be projected. */
|
|
13
|
+
export function apiKeyPrincipal(record: unknown, digest: string): ApiKeyPrincipal | undefined;
|
|
14
|
+
/** Replace all authority assertions; callers must supply a live authenticated principal. */
|
|
15
|
+
export function forwardPrincipalAssertions(headers: Headers, principal: AdmissionPrincipal): void;
|