cortico 0.1.0 → 0.1.2
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 +7 -14
- package/package.json +1 -3
- package/src/boot.ts +18 -0
- package/src/bot.ts +41 -6
- package/src/core/README.md +4 -6
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +15 -5
- package/src/core/loop.ts +55 -27
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +19 -3
- package/src/core/util.ts +17 -1
- package/src/deploy.ts +4 -1
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +23 -2
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +23 -9
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +32 -8
- package/src/providers/base.ts +3 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +8 -18
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +8 -6
- package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
- package/src/providers/llamacpp/console/server.ts +3 -1
- package/src/providers/llamacpp/index.ts +3 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/name.ts +8 -0
- package/src/providers/openai-responses-compat/config.ts +13 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +127 -0
- package/src/providers/openai-responses-compat/index.ts +38 -13
- package/src/providers/openai-responses-compat/native.ts +9 -4
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/registry.ts +5 -0
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +11 -6
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
- package/src/web/client/console-pages/host.ts +24 -7
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/core/strings.ts +2 -2
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +2 -2
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +34 -11
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +20 -14
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/providers/detail.ts +262 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +173 -87
- package/src/web/client/features/providers/strings.ts +44 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +13 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +28 -48
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +14 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/console-pages.ts +1 -1
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +143 -26
- package/src/web/server.ts +233 -19
- package/src/web/shared/client-panel.ts +4 -1
- package/src/web/shared/console-protocol.ts +4 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +69 -11
- package/src/worlds/minecraft/cell-facts.ts +226 -0
- package/src/worlds/minecraft/chests.ts +5 -0
- package/src/worlds/minecraft/containers.ts +325 -0
- package/src/worlds/minecraft/entity-facts.ts +31 -5
- package/src/worlds/minecraft/executor.ts +283 -10348
- package/src/worlds/minecraft/inventory.ts +268 -0
- package/src/worlds/minecraft/melee.ts +419 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/minecraft/placed-ledger.ts +152 -0
- package/src/worlds/minecraft/placement.ts +1038 -0
- package/src/worlds/minecraft/receipt.ts +340 -0
- package/src/worlds/minecraft/skill-context.ts +358 -0
- package/src/worlds/minecraft/skills-build.ts +1169 -0
- package/src/worlds/minecraft/skills-container.ts +1343 -0
- package/src/worlds/minecraft/skills-craft.ts +333 -0
- package/src/worlds/minecraft/skills-dig.ts +624 -0
- package/src/worlds/minecraft/skills-gather.ts +1230 -0
- package/src/worlds/minecraft/skills-interact.ts +1559 -0
- package/src/worlds/minecraft/tools.ts +331 -0
- package/src/worlds/minecraft/travel.ts +763 -0
- package/src/worlds/minecraft/until.ts +75 -0
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- package/src/worlds/terminal/world.ts +5 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning section of one endpoint: the replay form, written on change, and the probe that
|
|
3
|
+
* determines it. The endpoint comes from `ctx.scope.instance`.
|
|
4
|
+
*/
|
|
5
|
+
import type { ConsolePanel, ConsolePanelContext } from '../../../web/shared/client-panel.ts';
|
|
6
|
+
import { configField } from '../../../web/client/features/config/view.ts';
|
|
7
|
+
import type { DetectResult, ProbeOutcome, ReasoningPanelState } from './server.ts';
|
|
8
|
+
import { panel } from '../strings.ts';
|
|
9
|
+
|
|
10
|
+
export const reasoningPanel: ConsolePanel = {
|
|
11
|
+
mount: async (ctx: ConsolePanelContext) => {
|
|
12
|
+
const { ui, root } = ctx;
|
|
13
|
+
const S = ctx.language === 'en' ? panel.en : panel.zh;
|
|
14
|
+
const name = ctx.scope.instance;
|
|
15
|
+
const path = `providers.${name}.options.reasoningReplay`;
|
|
16
|
+
const textPath = `providers.${name}.options.syntheticReasoningText`;
|
|
17
|
+
const labels: Record<string, string> = { encrypted: S.encrypted, plaintext: S.plaintext };
|
|
18
|
+
const card = ui.sheet({ title: S.title });
|
|
19
|
+
const message = ui.msgline();
|
|
20
|
+
root.append(card.el, message);
|
|
21
|
+
|
|
22
|
+
const outcome = (probe: ProbeOutcome | undefined): string =>
|
|
23
|
+
!probe ? S.skipped : probe.ok ? S.accepted : S.rejected(probe.status, probe.error);
|
|
24
|
+
const describe = (result: DetectResult): string =>
|
|
25
|
+
`${S.outcome(outcome(result.bare), outcome(result.withReasoning))}。${result.verdict ? S.applied(labels[result.verdict]) : S.undetermined}`;
|
|
26
|
+
|
|
27
|
+
async function save(groupId: string, key: string, value: string): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
await ctx.setConfig(groupId, { [key]: value });
|
|
30
|
+
message.textContent = S.saved;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
message.textContent = String(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function detect(button: HTMLButtonElement): Promise<void> {
|
|
37
|
+
const release = ui.disable(button);
|
|
38
|
+
message.textContent = S.detecting;
|
|
39
|
+
try {
|
|
40
|
+
message.textContent = describe(await ctx.invoke<DetectResult>('detect', [{ name }]));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
message.textContent = String(error);
|
|
43
|
+
} finally {
|
|
44
|
+
release.dispose();
|
|
45
|
+
}
|
|
46
|
+
await load();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function load(): Promise<void> {
|
|
50
|
+
let state: ReasoningPanelState;
|
|
51
|
+
try {
|
|
52
|
+
state = await ctx.invoke<ReasoningPanelState>('state', [{ name }]);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
message.textContent = String(error);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (ctx.signal.aborted) return;
|
|
58
|
+
const { group, values } = state.config[0];
|
|
59
|
+
const property = group.schema.properties[path];
|
|
60
|
+
const current = values[path];
|
|
61
|
+
const select = ui.select({
|
|
62
|
+
value: typeof current === 'string' && current ? current : 'encrypted',
|
|
63
|
+
options: Object.entries(labels).map(([value, label]) => ({ value, label })),
|
|
64
|
+
onChange: (value) => void save(group.id, path, value),
|
|
65
|
+
});
|
|
66
|
+
select.setAttribute('aria-label', property.title);
|
|
67
|
+
const button = ui.button(S.detect, { onClick: () => void detect(button) });
|
|
68
|
+
const row = ui.rowbar();
|
|
69
|
+
row.append(select, button);
|
|
70
|
+
card.body.replaceChildren(ui.field(property.title, row));
|
|
71
|
+
if (property.description) card.body.append(ui.msgline(property.description));
|
|
72
|
+
|
|
73
|
+
const textProperty = group.schema.properties[textPath];
|
|
74
|
+
const text = configField(ui, textProperty, values[textPath], () => {
|
|
75
|
+
if (text.read) void save(group.id, textPath, String(text.read()).trim());
|
|
76
|
+
}, ctx.signal);
|
|
77
|
+
text.node.setAttribute('aria-label', textProperty.title);
|
|
78
|
+
card.body.append(ui.field(textProperty.title, text.node));
|
|
79
|
+
if (textProperty.description) card.body.append(ui.msgline(textProperty.description));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
await load();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reasoning section of the endpoint page: the replay form and its probe. The probe sends the
|
|
3
|
+
* shape production sends after an event delivery, a function call the model never made followed by
|
|
4
|
+
* its output, once without reasoning and once with plaintext reasoning, and stores the form the
|
|
5
|
+
* endpoint accepts.
|
|
6
|
+
*/
|
|
7
|
+
import type { ConsolePageContribution } from '../../../web/shared/console-protocol.ts';
|
|
8
|
+
import { PROBE_MAX_OUTPUT_TOKENS, type ProviderConsoleHost } from '../../console/types.ts';
|
|
9
|
+
import { getByPath, type ConfigGroup, type ConfigValues } from '../../../core/config-schema.ts';
|
|
10
|
+
import type { LLMProviderEntry, ModelSpec } from '../../../core/types.ts';
|
|
11
|
+
import { GenerationError, type Generation, type ResponseClient } from '../../../core/generation.ts';
|
|
12
|
+
import { functionCall, functionResult, message } from '../../../protocol/open-responses/context.ts';
|
|
13
|
+
import { responseRequest } from '../../../protocol/open-responses/context-helpers.ts';
|
|
14
|
+
import type { ReasoningReplay } from '../../transport/responses-input.ts';
|
|
15
|
+
import { protocolConfig } from '../config.ts';
|
|
16
|
+
import type { CompatControl } from '../index.ts';
|
|
17
|
+
import { text } from '../strings.ts';
|
|
18
|
+
|
|
19
|
+
/** The function the probe's synthetic call names; the endpoint sees a call the model never made. */
|
|
20
|
+
const PROBE_TOOL = 'probe_event_frame';
|
|
21
|
+
|
|
22
|
+
export type ProbeOutcome =
|
|
23
|
+
| { ok: true; status: number | null; reasoning: 'encrypted' | 'plaintext' | 'none' }
|
|
24
|
+
| { ok: false; status: number | null; error: string };
|
|
25
|
+
|
|
26
|
+
export interface DetectResult {
|
|
27
|
+
/** The form written to the entry; null leaves the entry unchanged. */
|
|
28
|
+
verdict: ReasoningReplay | null;
|
|
29
|
+
bare: ProbeOutcome;
|
|
30
|
+
/** Absent when the bare probe failed for a reason other than a rejected request. */
|
|
31
|
+
withReasoning?: ProbeOutcome;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ReasoningPanelState {
|
|
35
|
+
name: string;
|
|
36
|
+
config: Array<{ group: ConfigGroup; values: ConfigValues }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Both accepted: the form the endpoint returned, with a reasoning-free response counted as
|
|
41
|
+
* encrypted since nothing is replayed either way. One rejected with 400: the other form.
|
|
42
|
+
* Anything else is undetermined.
|
|
43
|
+
*/
|
|
44
|
+
export function replayVerdict(bare: ProbeOutcome, withReasoning: ProbeOutcome): ReasoningReplay | null {
|
|
45
|
+
if (bare.ok && withReasoning.ok) return bare.reasoning === 'plaintext' ? 'plaintext' : 'encrypted';
|
|
46
|
+
if (withReasoning.ok && bare.status === 400) return 'plaintext';
|
|
47
|
+
if (bare.ok && withReasoning.status === 400) return 'encrypted';
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function probeRequest(spec: ModelSpec) {
|
|
52
|
+
const context = [
|
|
53
|
+
message('user', 'Reply with one word.'),
|
|
54
|
+
functionCall('probe_1', PROBE_TOOL, '{}'),
|
|
55
|
+
functionResult('probe_1', 'delivered'),
|
|
56
|
+
];
|
|
57
|
+
const tools = [{ name: PROBE_TOOL, description: 'Delivers external events.', parameters: { type: 'object', properties: {} } }];
|
|
58
|
+
return {
|
|
59
|
+
...responseRequest(spec, context, tools),
|
|
60
|
+
max_output_tokens: Math.min(spec.maxTokens ?? PROBE_MAX_OUTPUT_TOKENS, PROBE_MAX_OUTPUT_TOKENS),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reasoningForm(generation: Generation): 'encrypted' | 'plaintext' | 'none' {
|
|
65
|
+
const items = generation.response.output.filter((item) => item.type === 'reasoning');
|
|
66
|
+
if (!items.length) return 'none';
|
|
67
|
+
return items.some((item) => item.encrypted_content) ? 'encrypted' : 'plaintext';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function probe(client: ResponseClient, spec: ModelSpec): Promise<ProbeOutcome> {
|
|
71
|
+
try {
|
|
72
|
+
const generation = await client.respond(probeRequest(spec), { diagnostic: true, nativeSpec: spec, role: 'probe' });
|
|
73
|
+
return { ok: true, status: generation.attempts.at(-1)?.status ?? null, reasoning: reasoningForm(generation) };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof GenerationError) return { ok: false, status: error.status, error: error.body || error.message };
|
|
76
|
+
return { ok: false, status: null, error: error instanceof Error ? error.message : String(error) };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function compatConsole(host: ProviderConsoleHost): Partial<ConsolePageContribution> {
|
|
81
|
+
const S = text(host.language);
|
|
82
|
+
const body = (args: unknown[]): string => {
|
|
83
|
+
const [raw] = args;
|
|
84
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error(S.bodyRequired);
|
|
85
|
+
const name = (raw as Record<string, unknown>).name;
|
|
86
|
+
if (typeof name !== 'string') throw new Error(S.instanceNameRequired);
|
|
87
|
+
return name;
|
|
88
|
+
};
|
|
89
|
+
const entryOf = (name: string): LLMProviderEntry => {
|
|
90
|
+
const found = host.entries().find((entry) => entry.name === name);
|
|
91
|
+
if (!found) throw new Error(S.instanceNameRequired);
|
|
92
|
+
return found.entry;
|
|
93
|
+
};
|
|
94
|
+
const detect = async (name: string): Promise<DetectResult> => {
|
|
95
|
+
const entry = entryOf(name);
|
|
96
|
+
if (!entry.spec) throw new Error(S.modelRequired);
|
|
97
|
+
if (!entry.spec.thinking) throw new Error(S.thinkingOff);
|
|
98
|
+
const control = host.instance(name).control as CompatControl;
|
|
99
|
+
const bare = await probe(control.probeClient('encrypted'), entry.spec);
|
|
100
|
+
if (!bare.ok && bare.status !== 400) return { verdict: null, bare };
|
|
101
|
+
const withReasoning = await probe(control.probeClient('plaintext'), entry.spec);
|
|
102
|
+
const verdict = replayVerdict(bare, withReasoning);
|
|
103
|
+
if (verdict) host.save(name, { ...entry, options: { ...entry.options, reasoningReplay: verdict } });
|
|
104
|
+
return { verdict, bare, withReasoning };
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
config: [],
|
|
108
|
+
panels: [{ id: 'reasoning', title: S.reasoningPanel, description: S.reasoningPanelDescription, slot: 'instance' }],
|
|
109
|
+
invoke: async (panel, method, args) => {
|
|
110
|
+
if (panel !== 'reasoning') throw new Error(S.unknownPanel);
|
|
111
|
+
const name = body(args);
|
|
112
|
+
if (method === 'state') {
|
|
113
|
+
const entry = entryOf(name);
|
|
114
|
+
const prefix = `providers.${name}.`;
|
|
115
|
+
const config = protocolConfig(name, entry, host.language).map((group) => ({
|
|
116
|
+
group,
|
|
117
|
+
values: Object.fromEntries(Object.keys(group.schema.properties).map((path) => [
|
|
118
|
+
path, getByPath(entry as unknown as Record<string, unknown>, path.slice(prefix.length)) ?? '',
|
|
119
|
+
])) as ConfigValues,
|
|
120
|
+
}));
|
|
121
|
+
return { name, config } satisfies ReasoningPanelState;
|
|
122
|
+
}
|
|
123
|
+
if (method === 'detect') return detect(name);
|
|
124
|
+
throw new Error(S.unknownMethod);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ProviderModule } from '../base.ts';
|
|
2
2
|
import type { LLMProviderEntry } from '../../core/types.ts';
|
|
3
|
+
import type { ResponseClient } from '../../core/generation.ts';
|
|
3
4
|
import { isContextOverflow } from '../transport/errors.ts';
|
|
4
|
-
import {
|
|
5
|
+
import { REASONING_REPLAYS, type ReasoningReplay } from '../transport/responses-input.ts';
|
|
6
|
+
import { ModelCatalog, ResponsesProvider, type ResponsesProviderOptions } from './native.ts';
|
|
5
7
|
import { text } from './strings.ts';
|
|
6
8
|
import { protocolConfig } from './config.ts';
|
|
9
|
+
import { compatConsole } from './console/server.ts';
|
|
7
10
|
|
|
8
11
|
/** Suggested base URLs for the console field; operators may enter other URLs. */
|
|
9
12
|
const BASE_URLS: readonly string[] = [
|
|
@@ -20,15 +23,25 @@ export interface CompatOptions {
|
|
|
20
23
|
endpointPath?: string;
|
|
21
24
|
extraHeaders?: Record<string, string>;
|
|
22
25
|
extraBody?: Record<string, unknown>;
|
|
26
|
+
/** Default `encrypted`. */
|
|
27
|
+
reasoningReplay?: ReasoningReplay;
|
|
28
|
+
/** Plaintext replay only; defaults to `SYNTHETIC_REASONING_TEXT`. */
|
|
29
|
+
syntheticReasoningText?: string;
|
|
23
30
|
}
|
|
24
31
|
export function compatOptions(entry: LLMProviderEntry): CompatOptions {
|
|
25
32
|
return (entry.options ?? {}) as CompatOptions;
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
/** Module-owned control object, used by the endpoint page's reasoning section. */
|
|
36
|
+
export interface CompatControl {
|
|
37
|
+
/** A client for this endpoint with the given replay form; the stored entry is unchanged. */
|
|
38
|
+
probeClient(replay: ReasoningReplay): ResponseClient;
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
/** Empty strings and empty objects are the console's "unset"; they do not reach the wire. */
|
|
29
42
|
function normalizeCompat(entry: LLMProviderEntry): LLMProviderEntry {
|
|
30
43
|
const options: Record<string, unknown> = { ...entry.options };
|
|
31
|
-
if (options
|
|
44
|
+
for (const key of ['endpointPath', 'reasoningReplay', 'syntheticReasoningText'] as const) if (options[key] === '') delete options[key];
|
|
32
45
|
for (const key of ['extraHeaders', 'extraBody'] as const) {
|
|
33
46
|
const value = options[key];
|
|
34
47
|
if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) delete options[key];
|
|
@@ -43,11 +56,13 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
43
56
|
export default {
|
|
44
57
|
id: 'openai-responses-compat',
|
|
45
58
|
title: 'OpenAI Responses Compatible',
|
|
59
|
+
description: 'Connect to Responses-compatible model services.',
|
|
60
|
+
localize: (language) => ({ description: language === 'zh' ? '连接兼容 Responses API 的模型服务。' : 'Connect to Responses-compatible model services.' }),
|
|
46
61
|
defaultBaseUrl: BASE_URLS[0],
|
|
47
62
|
baseUrlSuggestions: BASE_URLS,
|
|
48
63
|
normalize: normalizeCompat,
|
|
49
64
|
config: protocolConfig,
|
|
50
|
-
console:
|
|
65
|
+
console: compatConsole,
|
|
51
66
|
reasoningTiers: [],
|
|
52
67
|
effortSuggestions: EFFORTS,
|
|
53
68
|
serviceTiers: [],
|
|
@@ -60,6 +75,12 @@ export default {
|
|
|
60
75
|
|| Object.values(options.extraHeaders).some((value) => typeof value !== 'string')))
|
|
61
76
|
throw new Error(S.extraHeadersObject);
|
|
62
77
|
if (options.extraBody !== undefined && !isPlainObject(options.extraBody)) throw new Error(S.extraBodyObject);
|
|
78
|
+
if (options.reasoningReplay !== undefined && !REASONING_REPLAYS.includes(options.reasoningReplay))
|
|
79
|
+
throw new Error(S.reasoningReplayValue);
|
|
80
|
+
// 端点拒收空的 reasoning_text:全空白与空串一样会让明文形态整条请求 400。
|
|
81
|
+
if (options.syntheticReasoningText !== undefined
|
|
82
|
+
&& (typeof options.syntheticReasoningText !== 'string' || !options.syntheticReasoningText.trim()))
|
|
83
|
+
throw new Error(S.syntheticReasoningTextValue);
|
|
63
84
|
},
|
|
64
85
|
contextOverflow: isContextOverflow,
|
|
65
86
|
create(name, entry, host) {
|
|
@@ -70,20 +91,24 @@ export default {
|
|
|
70
91
|
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
71
92
|
});
|
|
72
93
|
const catalog = new ModelCatalog(() => ({ baseUrl: entry.baseUrl, headers: headers() }));
|
|
94
|
+
const clientOptions = (reasoningReplay: ReasoningReplay | undefined): ResponsesProviderOptions => ({
|
|
95
|
+
baseUrl: entry.baseUrl,
|
|
96
|
+
apiKey,
|
|
97
|
+
endpointPath: options.endpointPath,
|
|
98
|
+
extraHeaders: options.extraHeaders,
|
|
99
|
+
extraBody: options.extraBody,
|
|
100
|
+
log: host.log,
|
|
101
|
+
media: { enabled: () => entry.multimodal === true, read: host.readBlob },
|
|
102
|
+
keepThinking: host.keepThinking,
|
|
103
|
+
reasoningReplay,
|
|
104
|
+
syntheticReasoningText: options.syntheticReasoningText,
|
|
105
|
+
});
|
|
73
106
|
return {
|
|
74
107
|
listModels: () => catalog.list(),
|
|
75
108
|
contextWindow: (model) => catalog.contextWindow(model),
|
|
76
109
|
compatibilityKey: () => [options.endpointPath ?? '/responses', name],
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
apiKey,
|
|
80
|
-
endpointPath: options.endpointPath,
|
|
81
|
-
extraHeaders: options.extraHeaders,
|
|
82
|
-
extraBody: options.extraBody,
|
|
83
|
-
log: host.log,
|
|
84
|
-
media: { enabled: () => entry.multimodal === true, read: host.readBlob },
|
|
85
|
-
keepThinking: host.keepThinking,
|
|
86
|
-
}),
|
|
110
|
+
control: { probeClient: (replay) => new ResponsesProvider(clientOptions(replay)) } satisfies CompatControl,
|
|
111
|
+
client: new ResponsesProvider(clientOptions(options.reasoningReplay)),
|
|
87
112
|
};
|
|
88
113
|
},
|
|
89
114
|
} satisfies ProviderModule;
|
|
@@ -7,7 +7,7 @@ import { createResponse, type Request } from '../../protocol/open-responses/inde
|
|
|
7
7
|
import { standardUsage, type GenerateOptions } from '../../core/generation.ts';
|
|
8
8
|
import { NativeResponseAssembly, type ResponseAssembly, type parseChatResponse } from '../transport/response-assembly.ts';
|
|
9
9
|
import { responseMeters } from '../transport/response-meters.ts';
|
|
10
|
-
import { responsesInput, type ResponsesInputOptions } from '../transport/responses-input.ts';
|
|
10
|
+
import { responsesInput, type ReasoningReplay, type ResponsesInputOptions } from '../transport/responses-input.ts';
|
|
11
11
|
|
|
12
12
|
type Item = Record<string, unknown>;
|
|
13
13
|
|
|
@@ -21,13 +21,15 @@ export interface ResponsesProviderOptions {
|
|
|
21
21
|
extraBody?: Record<string, unknown>;
|
|
22
22
|
media?: ResponsesInputOptions['media'];
|
|
23
23
|
keepThinking?: () => boolean;
|
|
24
|
+
reasoningReplay?: ReasoningReplay;
|
|
25
|
+
syntheticReasoningText?: string;
|
|
24
26
|
log?: Logger;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
28
30
|
* Send reasoning only when an effort is supplied; otherwise leave it to the endpoint.
|
|
29
|
-
* Requests set store=false and request encrypted reasoning content for local stateless replay
|
|
30
|
-
* Endpoint extraBody fields are merged last.
|
|
31
|
+
* Requests set store=false and request encrypted reasoning content for local stateless replay;
|
|
32
|
+
* how recorded reasoning is replayed follows `reasoningReplay`. Endpoint extraBody fields are merged last.
|
|
31
33
|
*/
|
|
32
34
|
export function buildResponsesBody(
|
|
33
35
|
request: Request,
|
|
@@ -61,7 +63,10 @@ export class ResponsesProvider extends OpenAIHttpClient {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
protected override buildResponseBody(request: Request, options: GenerateOptions): Record<string, unknown> {
|
|
64
|
-
return buildResponsesBody(request, options, {
|
|
66
|
+
return buildResponsesBody(request, options, {
|
|
67
|
+
media: this.opts.media, keepThinking: this.opts.keepThinking, reasoningReplay: this.opts.reasoningReplay,
|
|
68
|
+
syntheticReasoningText: this.opts.syntheticReasoningText,
|
|
69
|
+
}, this.opts.extraBody);
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/** The Chat body builder is unreachable here: `buildResponseBody` is overridden whole. */
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { pick, type Language } from '../../core/language.ts';
|
|
2
2
|
|
|
3
|
-
/** Server side: ConfigGroup titles and validation errors. */
|
|
3
|
+
/** Server side: ConfigGroup titles, panel titles and validation errors. */
|
|
4
4
|
const zh = {
|
|
5
5
|
endpointPath: 'Responses 端点路径',
|
|
6
6
|
endpointPathDescription: '相对供应地址;默认 /responses。',
|
|
7
7
|
endpointPathSlash: '端点路径必须以 / 开头',
|
|
8
8
|
extraHeadersObject: '附加请求头必须是字符串到字符串的对象',
|
|
9
9
|
extraBodyObject: '附加请求体字段必须是对象',
|
|
10
|
+
reasoningReplay: '思维链回传形态',
|
|
11
|
+
reasoningReplayDescription: '加密回签名块,明文回推理文字;上游接受哪种由端点决定,不确定就探测。',
|
|
12
|
+
reasoningReplayValue: '思维链回传形态只能是 encrypted 或 plaintext',
|
|
13
|
+
syntheticReasoningText: '合成推理填充',
|
|
14
|
+
syntheticReasoningTextDescription: (fallback: string) =>
|
|
15
|
+
`明文回传时,没有记录来源的工具调用前补的那一段推理,模型会读到。留空用默认「${fallback}」;端点拒收空串与全空白。`,
|
|
16
|
+
syntheticReasoningTextValue: '合成推理填充不能是空串或全空白:端点会拒收整条请求',
|
|
17
|
+
reasoningPanel: '思维链',
|
|
18
|
+
reasoningPanelDescription: '思维链回传给上游的形态。',
|
|
19
|
+
bodyRequired: '需要请求体',
|
|
20
|
+
instanceNameRequired: '需要端点名',
|
|
21
|
+
unknownPanel: '未知面板',
|
|
22
|
+
unknownMethod: '未知操作',
|
|
23
|
+
modelRequired: '先选模型',
|
|
24
|
+
thinkingOff: '这条端点关着思维链,回传形态无关',
|
|
10
25
|
};
|
|
11
26
|
const en: typeof zh = {
|
|
12
27
|
endpointPath: 'Responses endpoint path',
|
|
@@ -14,6 +29,52 @@ const en: typeof zh = {
|
|
|
14
29
|
endpointPathSlash: 'The endpoint path must start with /',
|
|
15
30
|
extraHeadersObject: 'Extra headers must be an object of strings',
|
|
16
31
|
extraBodyObject: 'Extra body fields must be an object',
|
|
32
|
+
reasoningReplay: 'Reasoning replay',
|
|
33
|
+
reasoningReplayDescription: 'Encrypted sends the signed block back, plaintext the reasoning text; which one the endpoint accepts is the endpoint\'s, probe when unsure.',
|
|
34
|
+
reasoningReplayValue: 'Reasoning replay must be encrypted or plaintext',
|
|
35
|
+
syntheticReasoningText: 'Synthetic reasoning text',
|
|
36
|
+
syntheticReasoningTextDescription: (fallback: string) =>
|
|
37
|
+
`In plaintext replay, the reasoning sent before a tool call with no recorded origin; the model reads it. Empty uses the default "${fallback}"; endpoints reject an empty or blank string.`,
|
|
38
|
+
syntheticReasoningTextValue: 'Synthetic reasoning text cannot be empty or blank: the endpoint rejects the whole request',
|
|
39
|
+
reasoningPanel: 'Reasoning',
|
|
40
|
+
reasoningPanelDescription: 'The form in which reasoning is sent back to the endpoint.',
|
|
41
|
+
bodyRequired: 'Request body required',
|
|
42
|
+
instanceNameRequired: 'Endpoint name required',
|
|
43
|
+
unknownPanel: 'Unknown panel',
|
|
44
|
+
unknownMethod: 'Unknown method',
|
|
45
|
+
modelRequired: 'Choose a model first',
|
|
46
|
+
thinkingOff: 'Reasoning is off on this endpoint; the replay form does not apply',
|
|
17
47
|
};
|
|
18
48
|
export type Text = typeof zh;
|
|
19
49
|
export const text = (language: Language) => pick(language, { zh, en });
|
|
50
|
+
|
|
51
|
+
/** Client side: the reasoning section of the endpoint page. */
|
|
52
|
+
const panelZh = {
|
|
53
|
+
title: '思维链',
|
|
54
|
+
encrypted: '加密',
|
|
55
|
+
plaintext: '明文',
|
|
56
|
+
detect: '我不知道,测一下',
|
|
57
|
+
detecting: '探测中',
|
|
58
|
+
saved: '已保存',
|
|
59
|
+
accepted: '通过',
|
|
60
|
+
rejected: (status: number | null, error: string) => `被拒${status ? ` ${status}` : ''}:${error}`,
|
|
61
|
+
skipped: '未测',
|
|
62
|
+
outcome: (bare: string, withReasoning: string) => `不带思维链的合成调用:${bare};带明文思维链:${withReasoning}`,
|
|
63
|
+
applied: (label: string) => `已设为${label}`,
|
|
64
|
+
undetermined: '判断不出,设置未改',
|
|
65
|
+
};
|
|
66
|
+
const panelEn: typeof panelZh = {
|
|
67
|
+
title: 'Reasoning',
|
|
68
|
+
encrypted: 'Encrypted',
|
|
69
|
+
plaintext: 'Plaintext',
|
|
70
|
+
detect: 'Not sure, test it',
|
|
71
|
+
detecting: 'Testing',
|
|
72
|
+
saved: 'Saved',
|
|
73
|
+
accepted: 'accepted',
|
|
74
|
+
rejected: (status: number | null, error: string) => `rejected${status ? ` ${status}` : ''}: ${error}`,
|
|
75
|
+
skipped: 'not tested',
|
|
76
|
+
outcome: (bare: string, withReasoning: string) => `Synthetic call without reasoning: ${bare}; with plaintext reasoning: ${withReasoning}`,
|
|
77
|
+
applied: (label: string) => `Set to ${label}`,
|
|
78
|
+
undetermined: 'Undetermined; the setting is unchanged',
|
|
79
|
+
};
|
|
80
|
+
export const panel = { zh: panelZh, en: panelEn };
|
|
@@ -64,6 +64,11 @@ export class ProviderRegistry {
|
|
|
64
64
|
return module;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Preview instances use a detached entry and never enter the live instance cache. */
|
|
68
|
+
preview(name: string, entry: LLMProviderEntry): ProviderInstance {
|
|
69
|
+
return new ProviderRegistry(() => ({ [name]: entry }), this.host, this.modules).resolve(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
67
72
|
resolve(name: string): ProviderInstance {
|
|
68
73
|
const raw = this.entries()[name];
|
|
69
74
|
if (!raw) throw new Error(`没有这个 LLM provider: ${name}`);
|
|
@@ -1,22 +1,45 @@
|
|
|
1
1
|
/** Stateless native Responses input: the whole context is replayed on every request. */
|
|
2
2
|
import type { Request } from '../../protocol/open-responses/index.ts';
|
|
3
|
-
import { inputItem } from '../../protocol/open-responses/context.ts';
|
|
3
|
+
import { inputItem, itemText, type ContextRecord } from '../../protocol/open-responses/context.ts';
|
|
4
4
|
import type { GenerateOptions } from '../../core/generation.ts';
|
|
5
5
|
import { requestContext } from './native-input.ts';
|
|
6
6
|
import type { CompatMediaOptions } from './history.ts';
|
|
7
7
|
|
|
8
8
|
type Item = Record<string, unknown>;
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* How past reasoning re-enters the context. `encrypted`: the signed `encrypted_content` block, only
|
|
12
|
+
* when the recorded origin matches this request. `plaintext`: the reasoning text itself, for
|
|
13
|
+
* endpoints whose thinking mode requires the text of every tool-call turn back.
|
|
14
|
+
*/
|
|
15
|
+
export const REASONING_REPLAYS = ['encrypted', 'plaintext'] as const;
|
|
16
|
+
export type ReasoningReplay = (typeof REASONING_REPLAYS)[number];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default `reasoning_text` sent before a function call without a recorded origin in plaintext
|
|
20
|
+
* replay. A missing field, an empty `content`, an empty string and a summary without
|
|
21
|
+
* `reasoning_text` are each rejected, so the padding is one character; it is non-whitespace in case
|
|
22
|
+
* an endpoint trims. It carries no prose by default: what these calls are is the Persona's to say.
|
|
23
|
+
* The operator replaces it per endpoint.
|
|
24
|
+
*/
|
|
25
|
+
export const SYNTHETIC_REASONING_TEXT = '-';
|
|
26
|
+
|
|
10
27
|
export interface ResponsesInputOptions {
|
|
11
28
|
media?: CompatMediaOptions;
|
|
12
29
|
/** Whether past reasoning re-enters the context; the first synthetic turn is exempt. */
|
|
13
30
|
keepThinking?: () => boolean;
|
|
31
|
+
/** Default `encrypted`. */
|
|
32
|
+
reasoningReplay?: ReasoningReplay;
|
|
33
|
+
/** Plaintext replay only; defaults to `SYNTHETIC_REASONING_TEXT`. Endpoints reject an empty one. */
|
|
34
|
+
syntheticReasoningText?: string;
|
|
14
35
|
}
|
|
15
36
|
|
|
16
37
|
/**
|
|
17
|
-
*
|
|
18
|
-
* compatibility domain and model match this request.
|
|
19
|
-
*
|
|
38
|
+
* Encrypted replay: a reasoning item re-enters only with `encrypted_content` and when the recorded
|
|
39
|
+
* instance, module, compatibility domain and model match this request. Plaintext replay: a reasoning
|
|
40
|
+
* item re-enters as `reasoning_text`; the turn after the last user message is always replayed, earlier
|
|
41
|
+
* turns follow `keepThinking`; a function call without an origin gets a synthetic reasoning item
|
|
42
|
+
* unless one already precedes it. System and developer text is joined into instructions.
|
|
20
43
|
*/
|
|
21
44
|
export function responsesInput(
|
|
22
45
|
request: Request,
|
|
@@ -25,20 +48,37 @@ export function responsesInput(
|
|
|
25
48
|
): { input: Item[]; instructions: string | undefined } {
|
|
26
49
|
const input: Item[] = [];
|
|
27
50
|
const systems = request.instructions ? [request.instructions] : [];
|
|
28
|
-
|
|
51
|
+
const plaintext = opts.reasoningReplay === 'plaintext';
|
|
52
|
+
const entries = requestContext(request, options);
|
|
53
|
+
const roundStart = plaintext ? lastUserMessage(entries) : -1;
|
|
54
|
+
entries.forEach((entry, index) => {
|
|
29
55
|
const item = entry.item;
|
|
30
56
|
if (item.type === 'reasoning') {
|
|
31
|
-
if (
|
|
32
|
-
|
|
57
|
+
if (plaintext) {
|
|
58
|
+
const text = itemText(item);
|
|
59
|
+
if (!text && !item.encrypted_content) return;
|
|
60
|
+
if (!entry.context.head && index < roundStart && opts.keepThinking?.() === false) return;
|
|
61
|
+
const wire = inputItem(entry) as Item;
|
|
62
|
+
if (text) wire.content = [{ type: 'reasoning_text', text }];
|
|
63
|
+
input.push(wire);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!item.encrypted_content) return;
|
|
67
|
+
if (!entry.context.head && opts.keepThinking?.() === false) return;
|
|
33
68
|
const owner = entry.context.origin;
|
|
34
69
|
const current = options.origin;
|
|
35
70
|
if (!owner || !current || owner.instance !== current.instance || owner.module !== current.module
|
|
36
|
-
|| owner.compatibilityDomain !== current.compatibilityDomain || owner.model !== request.model)
|
|
71
|
+
|| owner.compatibilityDomain !== current.compatibilityDomain || owner.model !== request.model) return;
|
|
37
72
|
}
|
|
38
73
|
if (item.type === 'message' && (item.role === 'system' || item.role === 'developer')) {
|
|
39
74
|
systems.push(typeof item.content === 'string' ? item.content : item.content.map(part => 'text' in part ? part.text : '').join(''));
|
|
40
|
-
|
|
75
|
+
return;
|
|
41
76
|
}
|
|
77
|
+
if (plaintext && item.type === 'function_call' && !entry.context.origin && input.at(-1)?.type !== 'reasoning')
|
|
78
|
+
input.push({
|
|
79
|
+
type: 'reasoning', id: `rs_${item.call_id}`, summary: [],
|
|
80
|
+
content: [{ type: 'reasoning_text', text: opts.syntheticReasoningText ?? SYNTHETIC_REASONING_TEXT }],
|
|
81
|
+
});
|
|
42
82
|
const wire = inputItem(entry) as Item;
|
|
43
83
|
if (opts.media?.enabled() && entry.context.blobs?.length && (item.type === 'message' || item.type === 'function_call_output')) {
|
|
44
84
|
const field = item.type === 'message' ? 'content' : 'output';
|
|
@@ -51,6 +91,15 @@ export function responsesInput(
|
|
|
51
91
|
wire[field] = parts;
|
|
52
92
|
}
|
|
53
93
|
input.push(wire);
|
|
54
|
-
}
|
|
94
|
+
});
|
|
55
95
|
return { input, instructions: systems.length ? systems.join('\n') : undefined };
|
|
56
96
|
}
|
|
97
|
+
|
|
98
|
+
/** Index of the last user message, -1 when there is none. */
|
|
99
|
+
function lastUserMessage(entries: readonly ContextRecord[]): number {
|
|
100
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
101
|
+
const item = entries[index].item;
|
|
102
|
+
if (item.type === 'message' && item.role === 'user') return index;
|
|
103
|
+
}
|
|
104
|
+
return -1;
|
|
105
|
+
}
|
package/src/web/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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 -->
|
|
1
|
+
<!-- Owner: src/web/server.ts, src/web/auth.ts, src/web/shared/console-protocol.ts, src/web/shared/client-panel.ts, src/web/client/console-pages/host.ts, src/web/client/console-pages/builtins/llm-settings/panel.ts, src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts, src/web/client/features/providers/index.ts, src/web/client/features/live/index.ts -->
|
|
2
2
|
|
|
3
3
|
# src/web
|
|
4
4
|
|
|
@@ -43,19 +43,24 @@ manifest 的 `CONSOLE_PROTOCOL_VERSION` 不匹配时,浏览器拒绝加载。
|
|
|
43
43
|
|
|
44
44
|
`WebApp` 默认监听 `127.0.0.1`,支持由依赖配置指定监听地址。从首选端口起最多尝试五个端口;
|
|
45
45
|
端口为 0 时仅申请一次系统分配。WebSocket 使用 `noServer` 分派 `/ws/debug`、`/ws/sessions` 和面板流。
|
|
46
|
-
所有请求与 upgrade 先校验 Host
|
|
47
|
-
写请求与 upgrade 再按 Host 校验 Origin,拒绝不匹配或无效的 Origin;缺少 Origin
|
|
46
|
+
所有请求与 upgrade 先校验 Host 头:只接受回环名、显式绑定的地址和 `allowedHosts` 里的名字,绑到通配地址时不校验。
|
|
47
|
+
写请求与 upgrade 再按 Host 校验 Origin,拒绝不匹配或无效的 Origin;缺少 Origin 时放行,Origin 是
|
|
48
|
+
`allowedHosts` 里的名字时放行。监听非回环地址而没有设访问密码时启动记一条 warn。
|
|
49
|
+
`auth.ts` 的 `ConsoleAuth` 在设了密码时把未登录的请求挡在各路由与 body 解析之前:`/` 给登录页,其余 401,
|
|
50
|
+
upgrade 断开;登录态是 HttpOnly Cookie 里的无状态签名令牌(见 [console.md](../../docs/console.md))。
|
|
48
51
|
|
|
49
52
|
`/assets` 提供 `dist/web` 资源。首页在最后一个 `</body>` 前注入带 hash 的入口。
|
|
50
53
|
扩展面板通过 `/assets/extensions/<包>/<版本>/<文件>` 提供,仅允许 manifest 声明的脚本与样式文件。
|
|
51
54
|
|
|
52
55
|
`createBot()` 通过 `WebAppDeps` 注入事件库、session、运行控制、配置、存储、`consolePageSources` 和扩展信息。
|
|
53
|
-
调试流以 `head` 提供本次 `Persona.sessionHead()` 的合成开头;它不属于持久 session。
|
|
54
|
-
`providersLamp` 汇总本地端点可用性,由状态灯接口提供给浏览器。
|
|
55
56
|
|
|
56
57
|
## 浏览器
|
|
57
58
|
|
|
58
|
-
`
|
|
59
|
+
`features/providers` 一次编辑一条端点,改动只进浏览器暂存,保存时整条提交。模块面板挂在所选端点
|
|
60
|
+
的作用域里,面板的 `setConfig` 同样进暂存;`llm-settings` 的服务接口保留,旧的 `llm:<kind>` 路由
|
|
61
|
+
转到这一页。`features/providers/drafts.ts` 的暂存按部署分 scope 存进 localStorage,不含 API Key;
|
|
62
|
+
这一页自己拦切换端点,并向路由登记离开保护。端点保存、删除或设为当前之后推送状态帧,终端页顶部
|
|
63
|
+
的统计行显示当前端点的名称与模型,点开进该端点。
|
|
59
64
|
|
|
60
65
|
`main.ts` 的 `FEATURES` 包含 `live`、`core`、`usage`、`provider`、`world`、`extensions`、`prompts`、
|
|
61
66
|
`appearance`、`settings`。贡献页由 manifest 加载,保留路由段 `provider` 交由 `ConsolePageHost` 处理。
|