cortico 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/package.json +43 -0
- package/src/boot.ts +46 -0
- package/src/bot.ts +1342 -0
- package/src/config-file.ts +41 -0
- package/src/core/README.md +92 -0
- package/src/core/billing.ts +26 -0
- package/src/core/blobs.ts +106 -0
- package/src/core/bus.ts +311 -0
- package/src/core/config-schema.ts +201 -0
- package/src/core/config.ts +262 -0
- package/src/core/core.ts +699 -0
- package/src/core/cost.ts +244 -0
- package/src/core/event-store.ts +304 -0
- package/src/core/fork.ts +113 -0
- package/src/core/generation.ts +118 -0
- package/src/core/instance-lock.ts +215 -0
- package/src/core/ipc-logger.ts +105 -0
- package/src/core/language.ts +62 -0
- package/src/core/log-context.ts +38 -0
- package/src/core/loop.ts +1858 -0
- package/src/core/markers.ts +48 -0
- package/src/core/prefix.ts +121 -0
- package/src/core/run.ts +95 -0
- package/src/core/secrets.ts +18 -0
- package/src/core/session.ts +56 -0
- package/src/core/sessions.ts +261 -0
- package/src/core/state.ts +104 -0
- package/src/core/template.ts +72 -0
- package/src/core/timers.ts +129 -0
- package/src/core/tool-log.ts +135 -0
- package/src/core/transcript.ts +92 -0
- package/src/core/truncate.ts +121 -0
- package/src/core/types.ts +1140 -0
- package/src/core/usage-log.ts +79 -0
- package/src/core/util.ts +386 -0
- package/src/deploy-listing.ts +160 -0
- package/src/deploy.ts +162 -0
- package/src/extensions/README.md +72 -0
- package/src/extensions/dry-mount.ts +390 -0
- package/src/extensions/manifest.ts +153 -0
- package/src/extensions/runtime.ts +42 -0
- package/src/extensions.ts +519 -0
- package/src/launcher.ts +285 -0
- package/src/paths.ts +155 -0
- package/src/protocol/open-responses/LICENSE +201 -0
- package/src/protocol/open-responses/README.md +14 -0
- package/src/protocol/open-responses/context-helpers.ts +27 -0
- package/src/protocol/open-responses/context-log.ts +59 -0
- package/src/protocol/open-responses/context.ts +89 -0
- package/src/protocol/open-responses/generated.ts +115 -0
- package/src/protocol/open-responses/index.ts +33 -0
- package/src/protocol/open-responses/openapi.json +4230 -0
- package/src/protocol/open-responses/stream.ts +146 -0
- package/src/protocol/open-responses/tokens.ts +31 -0
- package/src/providers/README.md +86 -0
- package/src/providers/base.ts +111 -0
- package/src/providers/configuration.ts +97 -0
- package/src/providers/console/config.ts +21 -0
- package/src/providers/console/settings.ts +443 -0
- package/src/providers/console/strings.ts +74 -0
- package/src/providers/console/types.ts +11 -0
- package/src/providers/llamacpp/archive.ts +148 -0
- package/src/providers/llamacpp/catalog.ts +247 -0
- package/src/providers/llamacpp/config.ts +36 -0
- package/src/providers/llamacpp/console/client.ts +6 -0
- package/src/providers/llamacpp/console/models-panel.ts +120 -0
- package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
- package/src/providers/llamacpp/console/server.ts +153 -0
- package/src/providers/llamacpp/index.ts +95 -0
- package/src/providers/llamacpp/native.ts +51 -0
- package/src/providers/llamacpp/options.ts +117 -0
- package/src/providers/llamacpp/runtime-store.ts +139 -0
- package/src/providers/llamacpp/runtime.ts +214 -0
- package/src/providers/llamacpp/server.ts +286 -0
- package/src/providers/llamacpp/strings.ts +230 -0
- package/src/providers/openai-responses-compat/config.ts +20 -0
- package/src/providers/openai-responses-compat/index.ts +89 -0
- package/src/providers/openai-responses-compat/native.ts +127 -0
- package/src/providers/openai-responses-compat/strings.ts +19 -0
- package/src/providers/pricebook.ts +75 -0
- package/src/providers/registry.ts +137 -0
- package/src/providers/strings.ts +71 -0
- package/src/providers/transport/chat.ts +156 -0
- package/src/providers/transport/errors.ts +48 -0
- package/src/providers/transport/history.ts +68 -0
- package/src/providers/transport/native-input.ts +83 -0
- package/src/providers/transport/native-types.ts +13 -0
- package/src/providers/transport/response-assembly.ts +191 -0
- package/src/providers/transport/response-http.ts +196 -0
- package/src/providers/transport/response-meters.ts +27 -0
- package/src/providers/transport/responses-input.ts +56 -0
- package/src/web/README.md +84 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
- package/src/web/client/console-pages/builtins.ts +10 -0
- package/src/web/client/console-pages/context.ts +135 -0
- package/src/web/client/console-pages/host.ts +633 -0
- package/src/web/client/console-pages/loader.ts +132 -0
- package/src/web/client/console-pages/strings.ts +90 -0
- package/src/web/client/console-pages/tools/strings.ts +49 -0
- package/src/web/client/console-pages/tools/view.ts +215 -0
- package/src/web/client/core/api.ts +200 -0
- package/src/web/client/core/language.ts +56 -0
- package/src/web/client/core/lifecycle.ts +132 -0
- package/src/web/client/core/router.ts +199 -0
- package/src/web/client/core/stream.ts +157 -0
- package/src/web/client/core/websocket.ts +82 -0
- package/src/web/client/features/appearance/index.ts +333 -0
- package/src/web/client/features/appearance/strings.ts +87 -0
- package/src/web/client/features/config/index.ts +11 -0
- package/src/web/client/features/config/strings.ts +41 -0
- package/src/web/client/features/config/view.ts +428 -0
- package/src/web/client/features/core/events.ts +197 -0
- package/src/web/client/features/core/index.ts +254 -0
- package/src/web/client/features/core/run.ts +56 -0
- package/src/web/client/features/core/runlog.ts +214 -0
- package/src/web/client/features/core/sessions.ts +53 -0
- package/src/web/client/features/core/strings.ts +157 -0
- package/src/web/client/features/extensions/index.ts +422 -0
- package/src/web/client/features/extensions/strings.ts +164 -0
- package/src/web/client/features/feature.ts +93 -0
- package/src/web/client/features/live/context.ts +280 -0
- package/src/web/client/features/live/fork.ts +156 -0
- package/src/web/client/features/live/index.ts +392 -0
- package/src/web/client/features/live/onboarding.ts +113 -0
- package/src/web/client/features/live/protocol.ts +122 -0
- package/src/web/client/features/live/sessions.ts +71 -0
- package/src/web/client/features/live/status.ts +68 -0
- package/src/web/client/features/live/strings.ts +252 -0
- package/src/web/client/features/live/timeline.ts +590 -0
- package/src/web/client/features/prompts/editor.ts +292 -0
- package/src/web/client/features/prompts/index.ts +346 -0
- package/src/web/client/features/prompts/strings.ts +136 -0
- package/src/web/client/features/prompts/view.ts +256 -0
- package/src/web/client/features/providers/index.ts +100 -0
- package/src/web/client/features/providers/strings.ts +23 -0
- package/src/web/client/features/settings/general.ts +28 -0
- package/src/web/client/features/settings/index.ts +82 -0
- package/src/web/client/features/settings/strings.ts +33 -0
- package/src/web/client/features/storage/index.ts +1 -0
- package/src/web/client/features/storage/strings.ts +45 -0
- package/src/web/client/features/storage/view.ts +154 -0
- package/src/web/client/features/usage/chart.ts +486 -0
- package/src/web/client/features/usage/index.ts +407 -0
- package/src/web/client/features/usage/labels.ts +46 -0
- package/src/web/client/features/usage/range.ts +36 -0
- package/src/web/client/features/usage/state.ts +37 -0
- package/src/web/client/features/usage/strings.ts +191 -0
- package/src/web/client/features/usage/tooltip.ts +86 -0
- package/src/web/client/features/usage/types.ts +96 -0
- package/src/web/client/features/worlds/index.ts +424 -0
- package/src/web/client/features/worlds/strings.ts +121 -0
- package/src/web/client/main.ts +273 -0
- package/src/web/client/shell/avatar.ts +233 -0
- package/src/web/client/shell/index.ts +549 -0
- package/src/web/client/shell/strings.ts +89 -0
- package/src/web/client/theme/handoff.ts +43 -0
- package/src/web/client/theme/palette.ts +109 -0
- package/src/web/client/theme/registry.ts +130 -0
- package/src/web/client/theme/storage.ts +72 -0
- package/src/web/client/theme/strings.ts +123 -0
- package/src/web/client/theme/studio.ts +366 -0
- package/src/web/client/ui/actions.ts +109 -0
- package/src/web/client/ui/data.ts +185 -0
- package/src/web/client/ui/dom.ts +23 -0
- package/src/web/client/ui/fields.ts +191 -0
- package/src/web/client/ui/format.ts +75 -0
- package/src/web/client/ui/icons.ts +183 -0
- package/src/web/client/ui/images.ts +91 -0
- package/src/web/client/ui/index.ts +89 -0
- package/src/web/client/ui/lamp.ts +125 -0
- package/src/web/client/ui/log.ts +98 -0
- package/src/web/client/ui/overlay.ts +184 -0
- package/src/web/client/ui/page.ts +8 -0
- package/src/web/client/ui/prompt-input.tsx +311 -0
- package/src/web/client/ui/sheet.ts +131 -0
- package/src/web/client/ui/strings.ts +71 -0
- package/src/web/console-pages.ts +367 -0
- package/src/web/files.ts +107 -0
- package/src/web/path-picker.ts +296 -0
- package/src/web/public/index.html +17 -0
- package/src/web/public/styles.css +1715 -0
- package/src/web/server.ts +1883 -0
- package/src/web/shared/client-panel.ts +603 -0
- package/src/web/shared/console-protocol.ts +560 -0
- package/src/web/shared/css.d.ts +1 -0
- package/src/web/shared/path-picker.ts +23 -0
- package/src/web/shared/theme.ts +256 -0
- package/src/web/theme-store.ts +37 -0
- package/src/world.ts +449 -0
- package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
- package/src/worlds/bilibili/README.md +176 -0
- package/src/worlds/bilibili/audience-admission.ts +995 -0
- package/src/worlds/bilibili/client.ts +528 -0
- package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
- package/src/worlds/bilibili/config.ts +185 -0
- package/src/worlds/bilibili/console/client.ts +202 -0
- package/src/worlds/bilibili/definition.ts +35 -0
- package/src/worlds/bilibili/gift-frame.ts +131 -0
- package/src/worlds/bilibili/normalize.ts +452 -0
- package/src/worlds/bilibili/overlay/announcement.ts +76 -0
- package/src/worlds/bilibili/overlay/assets.ts +82 -0
- package/src/worlds/bilibili/overlay/model.ts +494 -0
- package/src/worlds/bilibili/overlay/project.ts +202 -0
- package/src/worlds/bilibili/overlay/server.ts +286 -0
- package/src/worlds/bilibili/overlay/types.ts +217 -0
- package/src/worlds/bilibili/overlay/web/app.js +690 -0
- package/src/worlds/bilibili/overlay/web/editor.css +297 -0
- package/src/worlds/bilibili/overlay/web/editor.html +102 -0
- package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
- package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
- package/src/worlds/bilibili/overlay/web/styles.css +258 -0
- package/src/worlds/bilibili/protobuf.ts +123 -0
- package/src/worlds/bilibili/wire.ts +94 -0
- package/src/worlds/bilibili/world.ts +1389 -0
- package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
- package/src/worlds/console-fixture/console/client.ts +72 -0
- package/src/worlds/console-fixture/world.ts +73 -0
- package/src/worlds/index.ts +15 -0
- package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
- package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
- package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
- package/src/worlds/minecraft/README.md +917 -0
- package/src/worlds/minecraft/blueprint-plan.ts +915 -0
- package/src/worlds/minecraft/blueprint-registry.ts +566 -0
- package/src/worlds/minecraft/blueprint-repair.ts +236 -0
- package/src/worlds/minecraft/blueprint-resource.ts +272 -0
- package/src/worlds/minecraft/blueprint.ts +492 -0
- package/src/worlds/minecraft/body-lease.ts +340 -0
- package/src/worlds/minecraft/bridge.ts +749 -0
- package/src/worlds/minecraft/check.ts +651 -0
- package/src/worlds/minecraft/chests.ts +292 -0
- package/src/worlds/minecraft/client-launch.ts +272 -0
- package/src/worlds/minecraft/client-options.ts +85 -0
- package/src/worlds/minecraft/client-skins.ts +164 -0
- package/src/worlds/minecraft/client-window.ps1 +91 -0
- package/src/worlds/minecraft/client.ts +423 -0
- package/src/worlds/minecraft/combat-context.ts +109 -0
- package/src/worlds/minecraft/combat.ts +1552 -0
- package/src/worlds/minecraft/config.ts +499 -0
- package/src/worlds/minecraft/console/access.ts +218 -0
- package/src/worlds/minecraft/console/client.ts +244 -0
- package/src/worlds/minecraft/console/log.ts +110 -0
- package/src/worlds/minecraft/console/mount.ts +420 -0
- package/src/worlds/minecraft/console/skin.ts +257 -0
- package/src/worlds/minecraft/console/style.css +154 -0
- package/src/worlds/minecraft/console/world.ts +449 -0
- package/src/worlds/minecraft/deaths.ts +135 -0
- package/src/worlds/minecraft/definition.ts +18 -0
- package/src/worlds/minecraft/engine-child.ts +274 -0
- package/src/worlds/minecraft/engine-ipc.ts +98 -0
- package/src/worlds/minecraft/entity-facts.ts +132 -0
- package/src/worlds/minecraft/escape.ts +407 -0
- package/src/worlds/minecraft/executor.ts +14494 -0
- package/src/worlds/minecraft/explored.ts +156 -0
- package/src/worlds/minecraft/geometry.ts +205 -0
- package/src/worlds/minecraft/goal-plan.ts +647 -0
- package/src/worlds/minecraft/item-break.ts +126 -0
- package/src/worlds/minecraft/item-facts.ts +146 -0
- package/src/worlds/minecraft/item-pick.ts +67 -0
- package/src/worlds/minecraft/level-dat.ts +176 -0
- package/src/worlds/minecraft/log.ts +132 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
- package/src/worlds/minecraft/names.ts +377 -0
- package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
- package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
- package/src/worlds/minecraft/piglin.ts +49 -0
- package/src/worlds/minecraft/policy.ts +547 -0
- package/src/worlds/minecraft/precheck.ts +655 -0
- package/src/worlds/minecraft/proxy.ts +445 -0
- package/src/worlds/minecraft/ranged.ts +635 -0
- package/src/worlds/minecraft/readouts.ts +102 -0
- package/src/worlds/minecraft/round.ts +26 -0
- package/src/worlds/minecraft/search-observation.ts +92 -0
- package/src/worlds/minecraft/server-config.ts +467 -0
- package/src/worlds/minecraft/server.ts +658 -0
- package/src/worlds/minecraft/show.ts +84 -0
- package/src/worlds/minecraft/skills.ts +1763 -0
- package/src/worlds/minecraft/terrain.ts +1735 -0
- package/src/worlds/minecraft/window.ts +96 -0
- package/src/worlds/minecraft/works.ts +179 -0
- package/src/worlds/minecraft/world.ts +6438 -0
- package/src/worlds/qq/ENV_PROMPT.md +5 -0
- package/src/worlds/qq/config.ts +108 -0
- package/src/worlds/qq/console/client.ts +126 -0
- package/src/worlds/qq/console/events.ts +155 -0
- package/src/worlds/qq/console/gate.ts +232 -0
- package/src/worlds/qq/console/roster.ts +203 -0
- package/src/worlds/qq/console/style.css +65 -0
- package/src/worlds/qq/conversation.ts +28 -0
- package/src/worlds/qq/definition.ts +62 -0
- package/src/worlds/qq/driver.ts +375 -0
- package/src/worlds/qq/history-tools.ts +209 -0
- package/src/worlds/qq/normalize.ts +323 -0
- package/src/worlds/qq/qface-map.ts +282 -0
- package/src/worlds/qq/vision-prompt.ts +27 -0
- package/src/worlds/qq/vision.ts +642 -0
- package/src/worlds/qq/vlm.ts +142 -0
- package/src/worlds/qq/world.ts +1503 -0
- package/src/worlds/terminal/ENV_PROMPT.md +11 -0
- package/src/worlds/terminal/config.ts +9 -0
- package/src/worlds/terminal/definition.ts +14 -0
- package/src/worlds/terminal/world.ts +700 -0
- package/src/worlds/websearch/ENV_PROMPT.md +1 -0
- package/src/worlds/websearch/brave-client.ts +261 -0
- package/src/worlds/websearch/config.ts +75 -0
- package/src/worlds/websearch/definition.ts +20 -0
- package/src/worlds/websearch/world.ts +171 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createResponse, type OutputItem, type Request, type Response, type StreamEvent } from '../../protocol/open-responses/index.ts';
|
|
2
|
+
import { ResponseAccumulator, ResponseProtocolError } from '../../protocol/open-responses/stream.ts';
|
|
3
|
+
import { standardUsage, unknownMeters, type TokenMeters } from '../../core/generation.ts';
|
|
4
|
+
import { chatMeters, responseMeters } from './response-meters.ts';
|
|
5
|
+
|
|
6
|
+
export interface ResponseAssembly {
|
|
7
|
+
feed(payload: unknown, emit: (event: StreamEvent) => void): void;
|
|
8
|
+
finish(emit: (event: StreamEvent) => void): Response;
|
|
9
|
+
snapshot(): Response | null;
|
|
10
|
+
meters(): TokenMeters;
|
|
11
|
+
serviceTier(): string | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate native Responses Items before notifying observers.
|
|
16
|
+
* Normalize response.reasoning_text.* to the schema name response.reasoning.* before validation.
|
|
17
|
+
*/
|
|
18
|
+
export class NativeResponseAssembly implements ResponseAssembly {
|
|
19
|
+
private readonly accumulator = new ResponseAccumulator();
|
|
20
|
+
private usage = unknownMeters();
|
|
21
|
+
private tier: string | null = null;
|
|
22
|
+
feed(payload: unknown, emit: (event: StreamEvent) => void): void {
|
|
23
|
+
if (!payload || typeof payload !== 'object') throw new ResponseProtocolError('Invalid native Responses event');
|
|
24
|
+
if ('type' in payload && (payload.type === 'response.reasoning_text.delta' || payload.type === 'response.reasoning_text.done'))
|
|
25
|
+
payload = { ...payload, type: payload.type.replace('reasoning_text', 'reasoning') };
|
|
26
|
+
let event = payload as StreamEvent;
|
|
27
|
+
if ('response' in event) {
|
|
28
|
+
const raw = event.response;
|
|
29
|
+
if (!raw || typeof raw.id !== 'string' || typeof raw.model !== 'string' || !Array.isArray(raw.output)) throw new ResponseProtocolError('Invalid native Responses resource');
|
|
30
|
+
if (typeof raw.service_tier === 'string') this.tier = raw.service_tier;
|
|
31
|
+
if (raw.usage) this.usage = responseMeters(raw.usage);
|
|
32
|
+
event = { ...event, response: { ...createResponse(raw.id, { model: raw.model }), ...raw, usage: standardUsage(this.usage) } };
|
|
33
|
+
}
|
|
34
|
+
this.accumulator.accept(event);
|
|
35
|
+
emit(event);
|
|
36
|
+
if (event.type === 'error') throw new ResponseProtocolError(`Native response error: ${JSON.stringify(event)}`);
|
|
37
|
+
}
|
|
38
|
+
finish(): Response { return this.accumulator.finish(); }
|
|
39
|
+
snapshot(): Response | null { return this.accumulator.snapshot(); }
|
|
40
|
+
serviceTier(): string | null { return this.tier; }
|
|
41
|
+
meters(): TokenMeters { return structuredClone(this.usage); }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type NativeChunk = {
|
|
45
|
+
id?: string; model?: string; service_tier?: string;
|
|
46
|
+
choices?: Array<{ index?: number; finish_reason?: string | null; delta?: {
|
|
47
|
+
content?: string | null; refusal?: string | null; reasoning_content?: string | null;
|
|
48
|
+
tool_calls?: Array<{ index: number; id?: string; function?: { name?: string; arguments?: string } }>;
|
|
49
|
+
} }>;
|
|
50
|
+
usage?: Record<string, unknown> | null;
|
|
51
|
+
};
|
|
52
|
+
type MutableOutput = { id: string; type: string; status?: string; content?: any[]; summary?: any[]; call_id?: string; name?: string; arguments?: string; role?: string };
|
|
53
|
+
|
|
54
|
+
/** Chat's finish_reason closes its Items; a new tool index never closes a previous call. */
|
|
55
|
+
export class ChatResponseAssembly implements ResponseAssembly {
|
|
56
|
+
private readonly response: Response;
|
|
57
|
+
private readonly accumulator = new ResponseAccumulator();
|
|
58
|
+
private sequence = 0;
|
|
59
|
+
private started = false;
|
|
60
|
+
private finishReason: string | null = null;
|
|
61
|
+
private usage = unknownMeters();
|
|
62
|
+
private tier: string | null = null;
|
|
63
|
+
private readonly calls = new Map<number, number>();
|
|
64
|
+
private readonly textSlots = new Map<string, number>();
|
|
65
|
+
private readonly pendingCalls = new Map<number, NonNullable<NonNullable<NonNullable<NativeChunk['choices']>[number]['delta']>['tool_calls']>>();
|
|
66
|
+
private readonly output: MutableOutput[] = [];
|
|
67
|
+
/** The Item that received the last delta is the one a length or filter stop cut short. */
|
|
68
|
+
private lastTouched: number | null = null;
|
|
69
|
+
|
|
70
|
+
constructor(request: Request) { this.response = createResponse(`resp_${crypto.randomUUID()}`, request); }
|
|
71
|
+
|
|
72
|
+
private send(event: Record<string, unknown>, emit: (event: StreamEvent) => void): void {
|
|
73
|
+
const complete = structuredClone({ ...event, sequence_number: this.sequence++ }) as StreamEvent;
|
|
74
|
+
this.accumulator.accept(complete);
|
|
75
|
+
emit(complete);
|
|
76
|
+
}
|
|
77
|
+
private start(chunk: NativeChunk, emit: (event: StreamEvent) => void): void {
|
|
78
|
+
if (this.started) return;
|
|
79
|
+
if (chunk.id) this.response.id = chunk.id;
|
|
80
|
+
if (chunk.model) this.response.model = chunk.model;
|
|
81
|
+
this.send({ type: 'response.created', response: this.response }, emit);
|
|
82
|
+
this.started = true;
|
|
83
|
+
}
|
|
84
|
+
private add(index: number, item: MutableOutput, emit: (event: StreamEvent) => void): void {
|
|
85
|
+
if (this.output[index]) throw new ResponseProtocolError('Native Chat output indices overlap');
|
|
86
|
+
this.output[index] = item;
|
|
87
|
+
this.send({ type: 'response.output_item.added', output_index: index, item }, emit);
|
|
88
|
+
}
|
|
89
|
+
private text(kind: 'reasoning' | 'content' | 'refusal', delta: string, emit: (event: StreamEvent) => void): void {
|
|
90
|
+
let index = this.textSlots.get(kind);
|
|
91
|
+
if (index === undefined) {
|
|
92
|
+
index = this.output.length;
|
|
93
|
+
const item = kind === 'reasoning'
|
|
94
|
+
? { type: 'reasoning', id: `rs_${crypto.randomUUID()}`, summary: [], content: [] }
|
|
95
|
+
: { type: 'message', id: `msg_${crypto.randomUUID()}`, role: 'assistant', status: 'in_progress', content: [] };
|
|
96
|
+
this.add(index, item, emit);
|
|
97
|
+
this.textSlots.set(kind, index);
|
|
98
|
+
const part = kind === 'reasoning' ? { type: 'reasoning_text', text: '' }
|
|
99
|
+
: kind === 'refusal' ? { type: 'refusal', refusal: '' } : { type: 'output_text', text: '', annotations: [] };
|
|
100
|
+
this.send({ type: 'response.content_part.added', output_index: index, item_id: item.id, content_index: 0, part }, emit);
|
|
101
|
+
this.output[index].content = [part];
|
|
102
|
+
}
|
|
103
|
+
const item = this.output[index];
|
|
104
|
+
this.send({ type: kind === 'reasoning' ? 'response.reasoning.delta' : kind === 'refusal' ? 'response.refusal.delta' : 'response.output_text.delta',
|
|
105
|
+
output_index: index, item_id: item.id, content_index: 0, delta, ...(kind === 'content' ? { logprobs: [] } : {}) }, emit);
|
|
106
|
+
const field = kind === 'refusal' ? 'refusal' : 'text';
|
|
107
|
+
item.content![0][field] += delta;
|
|
108
|
+
this.lastTouched = index;
|
|
109
|
+
}
|
|
110
|
+
feed(payload: unknown, emit: (event: StreamEvent) => void): void {
|
|
111
|
+
const chunk = payload as NativeChunk;
|
|
112
|
+
this.start(chunk, emit);
|
|
113
|
+
if (chunk.usage) this.usage = chatMeters(chunk.usage);
|
|
114
|
+
if (chunk.service_tier) this.response.service_tier = this.tier = chunk.service_tier;
|
|
115
|
+
const choice = chunk.choices?.[0];
|
|
116
|
+
if (!choice) return;
|
|
117
|
+
if (choice.index !== undefined && choice.index !== 0) throw new ResponseProtocolError('Multiple Chat choices are unsupported');
|
|
118
|
+
const delta = choice.delta;
|
|
119
|
+
if (delta?.reasoning_content) this.text('reasoning', delta.reasoning_content, emit);
|
|
120
|
+
if (delta?.content) this.text('content', delta.content, emit);
|
|
121
|
+
if (delta?.refusal) this.text('refusal', delta.refusal, emit);
|
|
122
|
+
for (const call of delta?.tool_calls ?? []) {
|
|
123
|
+
if (!Number.isInteger(call.index) || call.index < 0) throw new ResponseProtocolError('Invalid native tool index');
|
|
124
|
+
const pending = this.pendingCalls.get(call.index) ?? [];
|
|
125
|
+
pending.push(call);
|
|
126
|
+
this.pendingCalls.set(call.index, pending);
|
|
127
|
+
}
|
|
128
|
+
for (const nativeIndex of [...this.pendingCalls.keys()].sort((a, b) => a - b)) {
|
|
129
|
+
if (!this.calls.has(nativeIndex) && nativeIndex !== this.calls.size) break;
|
|
130
|
+
for (const call of this.pendingCalls.get(nativeIndex)!) {
|
|
131
|
+
let index = this.calls.get(call.index);
|
|
132
|
+
if (index === undefined) {
|
|
133
|
+
if (!call.id || !call.function?.name) throw new ResponseProtocolError('Native function call lacks identity');
|
|
134
|
+
index = this.output.length;
|
|
135
|
+
this.calls.set(call.index, index);
|
|
136
|
+
this.add(index, { type: 'function_call', id: `fc_${crypto.randomUUID()}`, call_id: call.id,
|
|
137
|
+
name: call.function.name, arguments: '', status: 'in_progress' }, emit);
|
|
138
|
+
}
|
|
139
|
+
const item = this.output[index];
|
|
140
|
+
if ((call.id && call.id !== item.call_id) || (call.function?.name && call.function.name !== item.name)) throw new ResponseProtocolError('Native function identity changed');
|
|
141
|
+
const fragment = call.function?.arguments;
|
|
142
|
+
if (fragment) {
|
|
143
|
+
this.send({ type: 'response.function_call_arguments.delta', output_index: index, item_id: item.id, delta: fragment }, emit);
|
|
144
|
+
item.arguments += fragment;
|
|
145
|
+
}
|
|
146
|
+
this.lastTouched = index;
|
|
147
|
+
}
|
|
148
|
+
this.pendingCalls.delete(nativeIndex);
|
|
149
|
+
}
|
|
150
|
+
if (choice.finish_reason) this.finishReason = choice.finish_reason;
|
|
151
|
+
}
|
|
152
|
+
/** A length or content-filter stop cuts only the Item that was still being written; earlier Items are complete. */
|
|
153
|
+
finish(emit: (event: StreamEvent) => void): Response {
|
|
154
|
+
if (!this.finishReason) throw new ResponseProtocolError('Chat transport ended without finish_reason');
|
|
155
|
+
if (this.pendingCalls.size) throw new ResponseProtocolError('Missing native function index');
|
|
156
|
+
const incomplete = this.finishReason === 'length' || this.finishReason === 'content_filter';
|
|
157
|
+
for (let index = 0; index < this.output.length; index++) {
|
|
158
|
+
const item = this.output[index];
|
|
159
|
+
if (!item) throw new ResponseProtocolError('Missing native function index');
|
|
160
|
+
if (item.type === 'function_call') {
|
|
161
|
+
this.send({ type: 'response.function_call_arguments.done', output_index: index, item_id: item.id, arguments: item.arguments }, emit);
|
|
162
|
+
} else {
|
|
163
|
+
const part = item.content![0];
|
|
164
|
+
this.send({ type: 'response.content_part.done', output_index: index, item_id: item.id, content_index: 0, part }, emit);
|
|
165
|
+
}
|
|
166
|
+
if (item.type !== 'reasoning') item.status = incomplete && index === this.lastTouched ? 'incomplete' : 'completed';
|
|
167
|
+
this.send({ type: 'response.output_item.done', output_index: index, item }, emit);
|
|
168
|
+
}
|
|
169
|
+
this.response.output = this.output as OutputItem[];
|
|
170
|
+
this.response.usage = standardUsage(this.usage);
|
|
171
|
+
this.response.status = incomplete ? 'incomplete' : 'completed';
|
|
172
|
+
this.response.completed_at = Math.floor(Date.now() / 1000);
|
|
173
|
+
this.response.incomplete_details = incomplete ? { reason: this.finishReason === 'length' ? 'max_output_tokens' : 'content_filter' } : null;
|
|
174
|
+
this.send({ type: `response.${this.response.status}`, response: this.response }, emit);
|
|
175
|
+
return this.accumulator.finish();
|
|
176
|
+
}
|
|
177
|
+
snapshot(): Response | null { return this.accumulator.snapshot(); }
|
|
178
|
+
serviceTier(): string | null { return this.tier; }
|
|
179
|
+
meters(): TokenMeters { return structuredClone(this.usage); }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function parseChatResponse(raw: unknown, request: Request): { response: Response; meters: TokenMeters; serviceTier: string | null } {
|
|
183
|
+
const data = raw as { id?: string; model?: string; usage?: Record<string, unknown>; choices?: Array<{ index?: number; finish_reason?: string | null; message?: Record<string, any> }> };
|
|
184
|
+
const choice = data.choices?.[0] as { message?: Record<string, any>; finish_reason?: string | null } | undefined;
|
|
185
|
+
if (!choice?.message) throw new ResponseProtocolError('Native Chat response lacks choices[0].message');
|
|
186
|
+
const assembler = new ChatResponseAssembly(request);
|
|
187
|
+
assembler.feed({ ...data, choices: [{ index: 0, delta: { ...choice.message,
|
|
188
|
+
tool_calls: choice.message.tool_calls?.map((call: any, index: number) => ({ ...call, index })) }, finish_reason: choice.finish_reason }] }, () => {});
|
|
189
|
+
const response = assembler.finish(() => {});
|
|
190
|
+
return { response, meters: assembler.meters(), serviceTier: assembler.serviceTier() };
|
|
191
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { Logger } from '../../core/types.ts';
|
|
2
|
+
import type { Request, Response, StreamEvent } from '../../protocol/open-responses/index.ts';
|
|
3
|
+
import type { ItemOrigin } from '../../protocol/open-responses/context.ts';
|
|
4
|
+
import { GenerationError, priceUsage, unknownMeters, type GenerateOptions, type Generation, type ProviderAttempt, type PriceSnapshot, type TokenMeters } from '../../core/generation.ts';
|
|
5
|
+
import { ResponseProtocolError } from '../../protocol/open-responses/stream.ts';
|
|
6
|
+
import { LLMError, abortError, retryDelay, reqIdSuffix } from './errors.ts';
|
|
7
|
+
import type { ResponseAssembly } from './response-assembly.ts';
|
|
8
|
+
|
|
9
|
+
export interface ResponseTransport {
|
|
10
|
+
url: string;
|
|
11
|
+
body: Record<string, unknown>;
|
|
12
|
+
headers(): Record<string, string> | Promise<Record<string, string>>;
|
|
13
|
+
refresh(): Promise<boolean>;
|
|
14
|
+
assembly(): ResponseAssembly;
|
|
15
|
+
parse(raw: unknown): { response: Response; meters: TokenMeters; serviceTier: string | null };
|
|
16
|
+
failure(info: { model: string; role?: string; elapsedMs: number; status: number; requestId: string | null; body: string; message: string }, record: (attempts: ProviderAttempt[]) => void): Promise<boolean>;
|
|
17
|
+
succeeded(): void;
|
|
18
|
+
log: Logger;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** SSE framing is independent of packet and line boundaries. */
|
|
22
|
+
export class EventDecoder {
|
|
23
|
+
private buffer = '';
|
|
24
|
+
private data: string[] = [];
|
|
25
|
+
feed(text: string): string[] {
|
|
26
|
+
this.buffer += text;
|
|
27
|
+
const events: string[] = [];
|
|
28
|
+
for (;;) {
|
|
29
|
+
const newline = this.buffer.indexOf('\n');
|
|
30
|
+
if (newline < 0) break;
|
|
31
|
+
const line = this.buffer.slice(0, newline).replace(/\r$/, '');
|
|
32
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
33
|
+
if (line === '') {
|
|
34
|
+
if (this.data.length) events.push(this.data.join('\n'));
|
|
35
|
+
this.data = [];
|
|
36
|
+
} else if (line.startsWith('data:')) this.data.push(line.slice(5).replace(/^ /, ''));
|
|
37
|
+
}
|
|
38
|
+
return events;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function irreversible(event: StreamEvent): boolean {
|
|
43
|
+
return event.type === 'response.output_text.delta' || event.type === 'response.refusal.delta'
|
|
44
|
+
|| event.type === 'response.function_call_arguments.delta'
|
|
45
|
+
|| (event.type === 'response.output_item.added' && event.item?.type === 'function_call');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 带内容的事件:增量,或输出项的开合。keepalive 帧与空事件不算。 */
|
|
49
|
+
function carriesContent(event: StreamEvent): boolean {
|
|
50
|
+
return 'delta' in event || event.type === 'response.output_item.added' || event.type === 'response.output_item.done';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 首包等待上限:流式 300 秒,非流式 120 秒。 */
|
|
54
|
+
const FIRST_RESPONSE_MS = { streaming: 300_000, unary: 120_000 } as const;
|
|
55
|
+
/** 帧空闲:连续这么久一个字节都没来。 */
|
|
56
|
+
const FRAME_IDLE_MS = 120_000;
|
|
57
|
+
/**
|
|
58
|
+
* 内容事件的空闲期限;keepalive 和空 delta 不重置它。
|
|
59
|
+
* 该计时器独立于字节接收计时,以覆盖持续收到空帧的响应。
|
|
60
|
+
*/
|
|
61
|
+
const CONTENT_IDLE_MS = 300_000;
|
|
62
|
+
|
|
63
|
+
/** Every fetch attempt produces its own immutable metering and price snapshot. */
|
|
64
|
+
export async function generate(request: Request, options: GenerateOptions, origin: ItemOrigin, transport: ResponseTransport): Promise<Generation> {
|
|
65
|
+
const generationId = crypto.randomUUID();
|
|
66
|
+
const attempts: ProviderAttempt[] = [];
|
|
67
|
+
let lastError: unknown;
|
|
68
|
+
let partial: Response | null = null;
|
|
69
|
+
let authRetried = false;
|
|
70
|
+
const delays = options.diagnostic ? [] : [1000, 4000, 10000];
|
|
71
|
+
const fail = (error: unknown): GenerationError => new GenerationError((error instanceof Error ? error.message : String(error)) + reqIdSuffix(attempts.filter(attempt => attempt.purpose !== 'diagnostic').at(-1)?.requestId), attempts,
|
|
72
|
+
partial, origin, error instanceof LLMError ? error.status : 0, error instanceof LLMError ? error.body : '', { cause: error });
|
|
73
|
+
for (let ordinal = 0; ordinal <= delays.length; ordinal++) {
|
|
74
|
+
try {
|
|
75
|
+
if (options.signal?.aborted) throw abortError(options.signal);
|
|
76
|
+
if (ordinal) await retryDelay(delays[ordinal - 1], options.signal);
|
|
77
|
+
} catch (error) { throw fail(error); }
|
|
78
|
+
const attempt: ProviderAttempt = {
|
|
79
|
+
id: crypto.randomUUID(), generationId, ordinal, origin: structuredClone(origin), startedAt: new Date().toISOString(), elapsedMs: 0,
|
|
80
|
+
requestId: null, responseId: null, outcome: 'failed', status: null, serviceTier: null, requestedServiceTier: typeof transport.body.service_tier === 'string' ? transport.body.service_tier : request.service_tier ?? null, purpose: options.diagnostic ? 'diagnostic' : 'generation', meters: unknownMeters(), charges: [],
|
|
81
|
+
};
|
|
82
|
+
let quotes: readonly PriceSnapshot[] = [];
|
|
83
|
+
const started = Date.now();
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
86
|
+
const streaming = Boolean(options.onEvent);
|
|
87
|
+
let timer = setTimeout(() => controller.abort(new Error('Provider first response timeout')), streaming ? FIRST_RESPONSE_MS.streaming : FIRST_RESPONSE_MS.unary);
|
|
88
|
+
let contentTimer: ReturnType<typeof setTimeout> | null = null;
|
|
89
|
+
const touchContent = (): void => {
|
|
90
|
+
if (contentTimer) clearTimeout(contentTimer);
|
|
91
|
+
contentTimer = setTimeout(() => controller.abort(new Error('Provider stream content idle timeout')), CONTENT_IDLE_MS);
|
|
92
|
+
};
|
|
93
|
+
let committed = false;
|
|
94
|
+
let characters = 0;
|
|
95
|
+
let runaway = false;
|
|
96
|
+
let observed = false;
|
|
97
|
+
const assembly = transport.assembly();
|
|
98
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
99
|
+
let sent = false;
|
|
100
|
+
let finishedAt: number | null = null;
|
|
101
|
+
const diagnostics: ProviderAttempt[] = [];
|
|
102
|
+
partial = null;
|
|
103
|
+
const forward = (event: StreamEvent): void => {
|
|
104
|
+
observed = true;
|
|
105
|
+
if (carriesContent(event)) touchContent();
|
|
106
|
+
if ('delta' in event && typeof event.delta === 'string') characters += event.delta.length;
|
|
107
|
+
if (irreversible(event)) committed = true;
|
|
108
|
+
if (!options.signal?.aborted) options.onEvent?.(event);
|
|
109
|
+
if (characters > (request.max_output_tokens ?? 32768) * 12) {
|
|
110
|
+
runaway = true;
|
|
111
|
+
controller.abort();
|
|
112
|
+
throw new LLMError('Provider exceeded the output character limit', 0, '');
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
try {
|
|
116
|
+
const headers = await transport.headers();
|
|
117
|
+
if (options.signal?.aborted) throw abortError(options.signal);
|
|
118
|
+
attempt.startedAt = new Date().toISOString();
|
|
119
|
+
quotes = structuredClone(options.quote?.({ startedAt: attempt.startedAt, requestedServiceTier: attempt.requestedServiceTier ?? null }) ?? []);
|
|
120
|
+
sent = true;
|
|
121
|
+
const response = await fetch(transport.url, { method: 'POST', body: JSON.stringify(transport.body), headers, signal });
|
|
122
|
+
attempt.status = response.status;
|
|
123
|
+
attempt.requestId = response.headers.get('x-request-id');
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
const error = new LLMError(`LLM API ${response.status}`, response.status, await response.text());
|
|
126
|
+
if (!options.diagnostic && (response.status === 401 || response.status === 403) && !authRetried && await transport.refresh()) {
|
|
127
|
+
authRetried = true;
|
|
128
|
+
lastError = error;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
if (streaming) {
|
|
134
|
+
if (!response.body) throw new LLMError('Streaming response has no body', response.status, '');
|
|
135
|
+
reader = response.body.getReader();
|
|
136
|
+
const decoder = new TextDecoder();
|
|
137
|
+
const frames = new EventDecoder();
|
|
138
|
+
touchContent();
|
|
139
|
+
stream: for (;;) {
|
|
140
|
+
const chunk = await reader.read();
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
timer = setTimeout(() => controller.abort(new Error('Provider stream idle timeout')), FRAME_IDLE_MS);
|
|
143
|
+
if (chunk.done) break;
|
|
144
|
+
for (const payload of frames.feed(decoder.decode(chunk.value, { stream: true }))) {
|
|
145
|
+
if (payload === '[DONE]') break stream;
|
|
146
|
+
assembly.feed(JSON.parse(payload), forward);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
partial = assembly.finish(forward);
|
|
150
|
+
attempt.meters = assembly.meters();
|
|
151
|
+
attempt.serviceTier = assembly.serviceTier();
|
|
152
|
+
} else {
|
|
153
|
+
const parsed = transport.parse(JSON.parse(await response.text()));
|
|
154
|
+
partial = parsed.response;
|
|
155
|
+
attempt.meters = parsed.meters;
|
|
156
|
+
attempt.serviceTier = parsed.serviceTier;
|
|
157
|
+
}
|
|
158
|
+
attempt.responseId = partial.id;
|
|
159
|
+
if (partial.status === 'failed') throw new LLMError(partial.error?.message ?? partial.error?.code ?? 'Response failed', 0, JSON.stringify(partial.error));
|
|
160
|
+
if (partial.status !== 'completed' && partial.status !== 'incomplete') throw new LLMError(`Response has no terminal status: ${partial.status}`, 0, '');
|
|
161
|
+
attempt.outcome = options.signal?.aborted ? 'discarded' : partial.status;
|
|
162
|
+
transport.succeeded();
|
|
163
|
+
return { response: partial, origin, attempts };
|
|
164
|
+
} catch (error) {
|
|
165
|
+
finishedAt = Date.now();
|
|
166
|
+
if (streaming) {
|
|
167
|
+
partial = assembly.snapshot();
|
|
168
|
+
attempt.meters = assembly.meters();
|
|
169
|
+
}
|
|
170
|
+
attempt.responseId = partial?.id ?? null;
|
|
171
|
+
if (options.signal?.aborted) {
|
|
172
|
+
attempt.outcome = 'aborted';
|
|
173
|
+
lastError = abortError(options.signal);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
lastError = error;
|
|
177
|
+
if (!sent || committed || runaway || (!streaming && error instanceof ResponseProtocolError)) break;
|
|
178
|
+
const status = error instanceof LLMError ? error.status : 0;
|
|
179
|
+
if (status !== 0 && status !== 429 && status < 500) break;
|
|
180
|
+
if (!options.diagnostic && observed && Date.now() - started >= 20000 && !await transport.failure({
|
|
181
|
+
model: request.model ?? '', role: options.role, elapsedMs: Date.now() - started, status,
|
|
182
|
+
requestId: attempt.requestId, body: error instanceof LLMError ? error.body : '', message: String(error),
|
|
183
|
+
}, values => diagnostics.push(...values))) break;
|
|
184
|
+
transport.log.warn('Provider attempt failed; retrying', { ordinal, err: String(error) });
|
|
185
|
+
} finally {
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
if (contentTimer) clearTimeout(contentTimer);
|
|
188
|
+
if (reader) { try { await reader.cancel(); } catch { /* The failed transport may already be closed. */ } reader.releaseLock(); }
|
|
189
|
+
attempt.elapsedMs = (finishedAt ?? Date.now()) - started;
|
|
190
|
+
attempt.charges = priceUsage(attempt.meters, quotes, attempt.serviceTier);
|
|
191
|
+
if (sent) attempts.push(attempt);
|
|
192
|
+
attempts.push(...diagnostics);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw fail(lastError);
|
|
196
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { unknownMeters, type TokenMeters } from '../../core/generation.ts';
|
|
2
|
+
|
|
3
|
+
/** A meter the provider did not report as a finite number stays unknown; the raw usage is kept beside it. */
|
|
4
|
+
function number(value: unknown): number | null {
|
|
5
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Native Chat Completions usage, including DeepSeek's explicit cache distribution. */
|
|
9
|
+
export function chatMeters(raw: Record<string, any> | null | undefined): TokenMeters {
|
|
10
|
+
if (!raw) return unknownMeters();
|
|
11
|
+
const input = number(raw.prompt_tokens);
|
|
12
|
+
const output = number(raw.completion_tokens);
|
|
13
|
+
const cachedInput = number(raw.prompt_cache_hit_tokens ?? raw.prompt_tokens_details?.cached_tokens);
|
|
14
|
+
const uncachedInput = number(raw.prompt_cache_miss_tokens) ?? (input !== null && cachedInput !== null ? input - cachedInput : null);
|
|
15
|
+
return { input, output, total: number(raw.total_tokens) ?? (input !== null && output !== null ? input + output : null),
|
|
16
|
+
cachedInput, uncachedInput, reasoning: number(raw.completion_tokens_details?.reasoning_tokens), native: structuredClone(raw) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function responseMeters(raw: Record<string, any> | null | undefined): TokenMeters {
|
|
20
|
+
if (!raw) return unknownMeters();
|
|
21
|
+
const input = number(raw.input_tokens);
|
|
22
|
+
const output = number(raw.output_tokens);
|
|
23
|
+
const cachedInput = number(raw.input_tokens_details?.cached_tokens);
|
|
24
|
+
const uncachedInput = input !== null && cachedInput !== null ? input - cachedInput : null;
|
|
25
|
+
return { input, output, total: number(raw.total_tokens) ?? (input !== null && output !== null ? input + output : null),
|
|
26
|
+
cachedInput, uncachedInput, reasoning: number(raw.output_tokens_details?.reasoning_tokens), native: structuredClone(raw) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Stateless native Responses input: the whole context is replayed on every request. */
|
|
2
|
+
import type { Request } from '../../protocol/open-responses/index.ts';
|
|
3
|
+
import { inputItem } from '../../protocol/open-responses/context.ts';
|
|
4
|
+
import type { GenerateOptions } from '../../core/generation.ts';
|
|
5
|
+
import { requestContext } from './native-input.ts';
|
|
6
|
+
import type { CompatMediaOptions } from './history.ts';
|
|
7
|
+
|
|
8
|
+
type Item = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
export interface ResponsesInputOptions {
|
|
11
|
+
media?: CompatMediaOptions;
|
|
12
|
+
/** Whether past reasoning re-enters the context; the first synthetic turn is exempt. */
|
|
13
|
+
keepThinking?: () => boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Replay reasoning only when encrypted_content exists and the recorded instance, module,
|
|
18
|
+
* compatibility domain and model match this request. These are local eligibility checks.
|
|
19
|
+
* Plaintext reasoning is omitted. System and developer text is joined into instructions.
|
|
20
|
+
*/
|
|
21
|
+
export function responsesInput(
|
|
22
|
+
request: Request,
|
|
23
|
+
options: GenerateOptions,
|
|
24
|
+
opts: ResponsesInputOptions = {},
|
|
25
|
+
): { input: Item[]; instructions: string | undefined } {
|
|
26
|
+
const input: Item[] = [];
|
|
27
|
+
const systems = request.instructions ? [request.instructions] : [];
|
|
28
|
+
for (const entry of requestContext(request, options)) {
|
|
29
|
+
const item = entry.item;
|
|
30
|
+
if (item.type === 'reasoning') {
|
|
31
|
+
if (!item.encrypted_content) continue;
|
|
32
|
+
if (!entry.context.head && opts.keepThinking?.() === false) continue;
|
|
33
|
+
const owner = entry.context.origin;
|
|
34
|
+
const current = options.origin;
|
|
35
|
+
if (!owner || !current || owner.instance !== current.instance || owner.module !== current.module
|
|
36
|
+
|| owner.compatibilityDomain !== current.compatibilityDomain || owner.model !== request.model) continue;
|
|
37
|
+
}
|
|
38
|
+
if (item.type === 'message' && (item.role === 'system' || item.role === 'developer')) {
|
|
39
|
+
systems.push(typeof item.content === 'string' ? item.content : item.content.map(part => 'text' in part ? part.text : '').join(''));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const wire = inputItem(entry) as Item;
|
|
43
|
+
if (opts.media?.enabled() && entry.context.blobs?.length && (item.type === 'message' || item.type === 'function_call_output')) {
|
|
44
|
+
const field = item.type === 'message' ? 'content' : 'output';
|
|
45
|
+
const content = wire[field];
|
|
46
|
+
const parts = typeof content === 'string' ? [{ type: 'input_text', text: content }] : [...(content as Item[])];
|
|
47
|
+
for (const ref of entry.context.blobs) {
|
|
48
|
+
const bytes = opts.media.read(ref.handle);
|
|
49
|
+
if (bytes) parts.push({ type: 'input_image', image_url: `data:${ref.mime};base64,${bytes.toString('base64')}` });
|
|
50
|
+
}
|
|
51
|
+
wire[field] = parts;
|
|
52
|
+
}
|
|
53
|
+
input.push(wire);
|
|
54
|
+
}
|
|
55
|
+
return { input, instructions: systems.length ? systems.join('\n') : undefined };
|
|
56
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<!-- Owner: src/web/server.ts, src/web/shared/console-protocol.ts, src/web/shared/client-panel.ts, src/web/client/console-pages/builtins/llm-settings/panel.ts -->
|
|
2
|
+
|
|
3
|
+
# src/web
|
|
4
|
+
|
|
5
|
+
控制台服务端、共享协议和浏览器代码。框架页面由控制台实现;World、Persona 与 Provider 通过声明贡献页面。
|
|
6
|
+
服务端聚合、校验并分发声明。
|
|
7
|
+
|
|
8
|
+
## 目录
|
|
9
|
+
|
|
10
|
+
| 路径 | 职责 |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `server.ts` | `WebApp`:Express、WebSocket、API 路由、静态资源和首页入口注入。 |
|
|
13
|
+
| `console-pages.ts` | `ConsolePageRegistry`、`ConsoleAssets`:聚合页面与资源,读取 `dist/web/asset-manifest.json`。 |
|
|
14
|
+
| `path-picker.ts` | 按平台调用本机路径选择器。 |
|
|
15
|
+
| `shared/console-protocol.ts` | 页面 id、manifest、路由常量与校验规则,供服务端和浏览器共用。 |
|
|
16
|
+
| `shared/client-panel.ts` | 浏览器面板接口:`ConsolePanelContext`、`ConsoleUi`、`ConsolePanel`。 |
|
|
17
|
+
| `client/core/` | API、路由、生命周期、流连接、WebSocket 与语言设置。 |
|
|
18
|
+
| `client/console-pages/` | 页面宿主、面板加载器、上下文与内置面板,如 `llm-settings`。 |
|
|
19
|
+
| `client/features/` | 框架页面,每页实现 `FrameworkFeature`。 |
|
|
20
|
+
| `client/ui/` | `ConsoleUi` 组件与图标。 |
|
|
21
|
+
| `client/shell/`、`client/theme/` | 页面框架与主题。 |
|
|
22
|
+
| `public/` | `index.html` 与 Tailwind 入口。 |
|
|
23
|
+
|
|
24
|
+
## 协议
|
|
25
|
+
|
|
26
|
+
`ConsolePageKind` 包含 `framework`、`world`、`persona`、`memory`、`llm`。贡献页 id 使用 `world:<id>`、
|
|
27
|
+
`persona:<id>`、`memory:<id>` 或 `llm:<id>`;构建脚本与服务端共用 `pageIdFor()`,`memory:<id>` 的浏览器产物
|
|
28
|
+
取 `persona:<id>` 那份。面板 id 在所属页内唯一。
|
|
29
|
+
manifest 的 `CONSOLE_PROTOCOL_VERSION` 不匹配时,浏览器拒绝加载。
|
|
30
|
+
|
|
31
|
+
`toPageManifest()` 将服务端的 `ConsolePageContribution` 转换为 `ConsolePageManifest`。
|
|
32
|
+
`invoke`、`promptDocs.path`、`config.schema` 和 `storage` 不发送给浏览器;`links`、`builtin` 与资源 URL 按协议规则校验。
|
|
33
|
+
链接拒绝 `javascript:`、`data:`,一页最多七盏状态灯。
|
|
34
|
+
|
|
35
|
+
| 路由 | 用途 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `/api/console/manifest` | 页面声明。 |
|
|
38
|
+
| `/api/console/lamps` | 状态灯。 |
|
|
39
|
+
| `/api/console/providers/<page>/panels/<panel>/<method>` | 面板调用;GET 只对面板 `getMethods` 点名的方法开放,参数使用 query 中的 JSON 数组;POST JSON 上限为 64 MiB。 |
|
|
40
|
+
| `/ws/providers/<page>/panels/<panel>` | 面板流。 |
|
|
41
|
+
|
|
42
|
+
## 服务端
|
|
43
|
+
|
|
44
|
+
`WebApp` 默认监听 `127.0.0.1`,支持由依赖配置指定监听地址。从首选端口起最多尝试五个端口;
|
|
45
|
+
端口为 0 时仅申请一次系统分配。WebSocket 使用 `noServer` 分派 `/ws/debug`、`/ws/sessions` 和面板流。
|
|
46
|
+
所有请求与 upgrade 先校验 Host 头:只接受回环名或显式绑定的地址,绑到通配地址时不校验。
|
|
47
|
+
写请求与 upgrade 再按 Host 校验 Origin,拒绝不匹配或无效的 Origin;缺少 Origin 时放行。
|
|
48
|
+
|
|
49
|
+
`/assets` 提供 `dist/web` 资源。首页在最后一个 `</body>` 前注入带 hash 的入口。
|
|
50
|
+
扩展面板通过 `/assets/extensions/<包>/<版本>/<文件>` 提供,仅允许 manifest 声明的脚本与样式文件。
|
|
51
|
+
|
|
52
|
+
`createBot()` 通过 `WebAppDeps` 注入事件库、session、运行控制、配置、存储、`consolePageSources` 和扩展信息。
|
|
53
|
+
调试流以 `head` 提供本次 `Persona.sessionHead()` 的合成开头;它不属于持久 session。
|
|
54
|
+
`providersLamp` 汇总本地端点可用性,由状态灯接口提供给浏览器。
|
|
55
|
+
|
|
56
|
+
## 浏览器
|
|
57
|
+
|
|
58
|
+
`llm-settings` 从端点状态中的 ConfigGroup 渲染连接字段与声明的请求路径,使用 `ctx.setConfig` 保存。
|
|
59
|
+
|
|
60
|
+
`main.ts` 的 `FEATURES` 包含 `live`、`core`、`usage`、`provider`、`world`、`extensions`、`prompts`、
|
|
61
|
+
`appearance`、`settings`。贡献页由 manifest 加载,保留路由段 `provider` 交由 `ConsolePageHost` 处理。
|
|
62
|
+
`features/config/view.ts` 与 `features/storage/view.ts` 是配置组与存储清单的通用视图,运行诊断页与
|
|
63
|
+
`ConsolePageHost` 共用。
|
|
64
|
+
|
|
65
|
+
`ConsolePageHost` 渲染页面声明中的徽标、状态灯、配置组、提示词文档、存储项与面板。
|
|
66
|
+
`ConsolePageLoader` 按页 id 查找 bundle,缓存成功的导入,校验默认导出的 `{ panels }`;缺少 `mount` 的面板显示错误。
|
|
67
|
+
卸载依次执行 abort、dispose、清空 root,并释放上下文登记的轮询、RAF、observer、监听器、请求和音频资源。
|
|
68
|
+
|
|
69
|
+
`ConsoleUi` 返回 DOM 节点,文本参数不作为 HTML 解析。面板通过 `signal` 管理资源。abort 会关闭关联的 toast、confirm 和 drawer,
|
|
70
|
+
待决 confirm 返回 `false`。
|
|
71
|
+
|
|
72
|
+
## 构建
|
|
73
|
+
|
|
74
|
+
`scripts/build-web.ts` 使用以下入口:
|
|
75
|
+
|
|
76
|
+
| 入口 | 资源键 |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `src/web/client/main.ts` | 控制台主入口。 |
|
|
79
|
+
| `src/worlds/*/console/client.ts` | `world:<name>` |
|
|
80
|
+
| `src/providers/*/console/client.ts` | `llm:<name>` |
|
|
81
|
+
| `bots/*/console/client.ts` | `persona:<name>` |
|
|
82
|
+
|
|
83
|
+
esbuild 输出分包 ESM 和带 hash 的文件名,写入 `asset-manifest.json`;Tailwind 输出 `styles.css`。
|
|
84
|
+
浏览器代码使用 `tsconfig.web.json` 检查:`pnpm typecheck:web`。
|