cozyclay 3.1.2 → 3.1.4
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/CHANGELOG.md +37 -0
- package/README.md +1 -1
- package/bin/agent/agent-runner.mjs +160 -151
- package/bin/agent/agent-stream.mjs +86 -0
- package/bin/agent/anthropic-subscription-events.mjs +120 -0
- package/bin/agent/anthropic-subscription-prompt.mjs +116 -0
- package/bin/agent/anthropic-subscription-stream.mjs +251 -0
- package/bin/agent/anthropic-subscription.mjs +73 -0
- package/bin/agent/claude-code-executable.mjs +93 -0
- package/bin/agent/cliproxy-provider.mjs +25 -11
- package/bin/agent/model-roles.mjs +112 -0
- package/bin/agent/openai-compatible-provider.mjs +95 -0
- package/bin/agent/pi-tools.mjs +19 -10
- package/bin/agent/provider-login.mjs +3 -1
- package/bin/agent/provider-routes.mjs +2 -0
- package/bin/agent/providers.mjs +35 -130
- package/bin/cozyclay.mjs +2 -48
- package/bin/star-prompt.mjs +123 -0
- package/dist/app/index.html +5 -5
- package/dist/assets/TaskBoard-U1Yl-4F1.js +1 -0
- package/dist/assets/app-BdRlMf4J.css +1 -0
- package/dist/assets/app-KXdgBC-S.js +5144 -0
- package/dist/assets/{motion3-compile-dgGhrvPK.js → motion3-compile-DDG3wklD.js} +1 -1
- package/dist/assets/scene-asset-sync-Bz59Ghdz.js +13 -0
- package/dist/assets/task-board-DFfNa0xg.js +15 -0
- package/dist/assets/{workflow-C87p6E1E.js → workflow-BMJGd_gZ.js} +1 -1
- package/dist/cozyclay-package.json +1 -1
- package/dist/workflow/index.html +3 -3
- package/package.json +6 -4
- package/src/App.jsx +28 -28
- package/src/agent-image-client.js +2 -2
- package/src/asset-pane.jsx +14 -14
- package/src/character-kind.js +13 -12
- package/src/commands/project.js +2 -1
- package/src/commands/shot.js +1 -1
- package/src/commands/view.js +2 -2
- package/src/domains/cast.js +15 -15
- package/src/domains/motion/motion-range-pin-rules.js +1 -1
- package/src/domains/motion.js +11 -11
- package/src/domains/objects.js +20 -30
- package/src/domains/scenes.js +14 -21
- package/src/domains/shots.js +3 -3
- package/src/first-success-guide.jsx +19 -3
- package/src/hierarchy-panel.jsx +6 -7
- package/src/locale-zh/app.js +124 -0
- package/src/locale-zh/domains.js +36 -0
- package/src/locale-zh/labels.js +148 -0
- package/src/locale-zh/panels.js +225 -0
- package/src/locale-zh/shell.js +230 -0
- package/src/locale-zh/timeline.js +145 -0
- package/src/locale-zh/workflow.js +104 -0
- package/src/locale-zh/workspace.js +190 -0
- package/src/locale-zh.js +21 -0
- package/src/locale.js +16 -6
- package/src/main.jsx +1 -1
- package/src/motion/beat-board-model.js +4 -4
- package/src/motion/beat-board.jsx +1 -1
- package/src/motion/ik-pick.js +24 -0
- package/src/motion/take-segments.jsx +5 -5
- package/src/motion/timeline.jsx +30 -40
- package/src/motion/waypoints.js +6 -6
- package/src/object-catalog.jsx +4 -5
- package/src/panels/CameraPanel.jsx +3 -3
- package/src/panels/HdrEnvironmentControls.jsx +2 -1
- package/src/panels/ObjectTransformPanel.jsx +19 -29
- package/src/panels/PoseImagePanel.jsx +8 -8
- package/src/panels/PosePanel.jsx +2 -2
- package/src/panels/ReferenceImageField.jsx +2 -2
- package/src/panels/RigControlPanel.jsx +2 -2
- package/src/panels/SculptPanel.jsx +9 -9
- package/src/panels/SubjectBox.jsx +3 -3
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/posestudio.jsx +10 -8
- package/src/project-browser.css +7 -6
- package/src/project-browser.jsx +7 -6
- package/src/project-open-file.js +19 -0
- package/src/qa-label.js +1 -1
- package/src/range-pin-panel.jsx +14 -14
- package/src/resource-status.jsx +3 -3
- package/src/result-modal.jsx +10 -10
- package/src/settings-menu.jsx +4 -1
- package/src/shell/BoardSlot.jsx +5 -5
- package/src/shell/CompositionGuideButton.jsx +1 -1
- package/src/shell/GhostsMenuGroup.jsx +1 -1
- package/src/shell/StatusBar.jsx +4 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +2 -2
- package/src/shell/details-selection.js +1 -1
- package/src/shell/preference-controls.jsx +2 -1
- package/src/shot-guides.js +5 -5
- package/src/stage-hdr.js +3 -3
- package/src/stage-labels-ko.js +2 -2
- package/src/stage-startup-defaults.js +3 -3
- package/src/studio-actions.js +2 -2
- package/src/styles/tokens.css +1 -1
- package/src/styles.css +1 -1
- package/src/use-case-question.jsx +18 -18
- package/src/workflow/AgentPanel.jsx +4 -4
- package/src/workflow/AgentPlanCard.jsx +1 -1
- package/src/workflow/AgentProgressCard.jsx +4 -4
- package/src/workflow/AgentTurnSteps.jsx +1 -1
- package/src/workflow/LoginPanel.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +1 -1
- package/src/workflow/TaskInline.jsx +1 -1
- package/src/workflow/agent-client.js +1 -1
- package/src/workflow/agent-turn.js +5 -5
- package/src/workflow/task-board.js +4 -4
- package/tools/loc-baseline.json +2 -2
- package/tools/run-tests.mjs +5 -5
- package/dist/assets/TaskBoard-GR8xZm5c.js +0 -1
- package/dist/assets/app-Bhe0TpGz.css +0 -1
- package/dist/assets/app-C4QTcDcb.js +0 -5144
- package/dist/assets/scene-asset-sync-qoxcdrhj.js +0 -13
- package/dist/assets/task-board-BqReHzOi.js +0 -15
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The provider call behind every model request of the agent runner (agent-runner.mjs). pi-agent-core 1.x's Agent does
|
|
2
|
+
// not retry a failed request (the removed AgentHarness did), so one request is retried here, in place:
|
|
3
|
+
// - a transient failure, by pi-ai's own isRetryableAssistantError (429, 5xx, overload, dropped connections), up to
|
|
4
|
+
// RETRY_POLICY.maxRetries times with pi-ai's backoff;
|
|
5
|
+
// - an expired Codex sign-in (401), once per turn, after a forced credential refresh (refreshAuth, the runner's).
|
|
6
|
+
// An attempt is retried only while nothing of it reached the Agent: its events are held until its first content event,
|
|
7
|
+
// so the transcript and the panel only ever see the attempt that answered. A retried request carries the same context,
|
|
8
|
+
// so no failed assistant message is stored and no completed tool runs again.
|
|
9
|
+
|
|
10
|
+
const RETRY_POLICY = Object.freeze({ maxRetries: 2, baseDelayMs: 1000 });
|
|
11
|
+
|
|
12
|
+
const CONTENT_EVENT = /^(text|thinking|toolcall)_/;
|
|
13
|
+
const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } };
|
|
14
|
+
|
|
15
|
+
/** Resolves after `ms`, or as soon as `signal` aborts. */
|
|
16
|
+
function pause(ms, signal) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
if (signal?.aborted) { resolve(); return; }
|
|
19
|
+
const timer = setTimeout(done, ms);
|
|
20
|
+
function done() { clearTimeout(timer); signal?.removeEventListener("abort", done); resolve(); }
|
|
21
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** An assistant message for a request that never produced one (pi-ai throws when a request has no credential). */
|
|
26
|
+
function failedMessage(model, error, signal) {
|
|
27
|
+
return {
|
|
28
|
+
role: "assistant", content: [], api: model.api, provider: model.provider, model: model.id, usage: EMPTY_USAGE,
|
|
29
|
+
stopReason: signal?.aborted ? "aborted" : "error",
|
|
30
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
31
|
+
timestamp: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The Agent's streamFn. `ai` is @earendil-works/pi-ai, `models` the registry the request runs on. `refreshAuth(model,
|
|
37
|
+
* message)` decides whether a failed attempt earns a credential refresh and one more try (it refreshes before answering
|
|
38
|
+
* true); `onAttempt(message)` sees every attempt's final message, failed ones included.
|
|
39
|
+
*/
|
|
40
|
+
export function createRetryingStream({ ai, models, refreshAuth = async () => false, onAttempt = () => {} }) {
|
|
41
|
+
return (model, context, options = {}) => {
|
|
42
|
+
const { signal } = options;
|
|
43
|
+
const out = ai.createAssistantMessageEventStream();
|
|
44
|
+
const shouldRetry = async (message, retries) => {
|
|
45
|
+
if (retries >= RETRY_POLICY.maxRetries || signal?.aborted) return false;
|
|
46
|
+
if (ai.isRetryableAssistantError(message)) {
|
|
47
|
+
await pause(ai.retryDelayMs(RETRY_POLICY, retries + 1), signal);
|
|
48
|
+
return !signal?.aborted;
|
|
49
|
+
}
|
|
50
|
+
return refreshAuth(model, message);
|
|
51
|
+
};
|
|
52
|
+
const finish = (message, held = []) => {
|
|
53
|
+
for (const early of held) out.push(early);
|
|
54
|
+
const failed = message.stopReason === "error" || message.stopReason === "aborted";
|
|
55
|
+
out.push(failed ? { type: "error", reason: message.stopReason, error: message } : { type: "done", reason: message.stopReason, message });
|
|
56
|
+
out.end();
|
|
57
|
+
};
|
|
58
|
+
void (async () => {
|
|
59
|
+
for (let retries = 0; ; retries += 1) {
|
|
60
|
+
// The Agent asks for the next request even after a cancelled tool: an aborted run makes no further model call.
|
|
61
|
+
if (signal?.aborted) { finish(failedMessage(model, new Error("Request was aborted"), signal)); return; }
|
|
62
|
+
const held = [];
|
|
63
|
+
let forwarding = false;
|
|
64
|
+
let message;
|
|
65
|
+
try {
|
|
66
|
+
const response = await models.streamSimple(model, context, options);
|
|
67
|
+
for await (const event of response) {
|
|
68
|
+
if (event.type === "done" || event.type === "error") break;
|
|
69
|
+
if (forwarding) { out.push(event); continue; }
|
|
70
|
+
held.push(event);
|
|
71
|
+
if (CONTENT_EVENT.test(event.type)) { forwarding = true; for (const early of held.splice(0)) out.push(early); }
|
|
72
|
+
}
|
|
73
|
+
message = await response.result();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
message = failedMessage(model, error, signal);
|
|
76
|
+
}
|
|
77
|
+
onAttempt(message);
|
|
78
|
+
if (!forwarding && message.stopReason === "error" && await shouldRetry(message, retries)) continue;
|
|
79
|
+
// An abort that landed during a backoff ends the request as aborted, like any other cancellation.
|
|
80
|
+
finish(message.stopReason === "error" && signal?.aborted ? { ...message, stopReason: "aborted" } : message, held);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// SDK stream-event → pi assistant-message event translation for the subscription lane, plus the
|
|
2
|
+
// output message scaffolding. The lane receives Anthropic's raw SSE protocol re-exported by the
|
|
3
|
+
// SDK (`stream_event` messages), so this mirrors what pi-ai's anthropic-messages api does with
|
|
4
|
+
// the same events.
|
|
5
|
+
|
|
6
|
+
/** The SDK names host tools mcp__cozyclay-tools__<name>; pi knows them by the bare name. */
|
|
7
|
+
const CUSTOM_TOOLS_MCP_PREFIX = "mcp__cozyclay-tools__";
|
|
8
|
+
|
|
9
|
+
const asRecord = (value) => (value !== null && typeof value === "object" && !Array.isArray(value) ? value : {});
|
|
10
|
+
|
|
11
|
+
export function mapStopReason(reason) {
|
|
12
|
+
if (reason === "tool_use") return "toolUse";
|
|
13
|
+
if (reason === "max_tokens") return "length";
|
|
14
|
+
return "stop";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function mapSdkToolNameToPi(name) {
|
|
18
|
+
return typeof name === "string" && name.startsWith(CUSTOM_TOOLS_MCP_PREFIX) ? name.slice(CUSTOM_TOOLS_MCP_PREFIX.length) : name;
|
|
19
|
+
}
|
|
20
|
+
export function updateUsage(model, output, usage, { reset = false } = {}) {
|
|
21
|
+
if (!usage) return;
|
|
22
|
+
if (reset) {
|
|
23
|
+
output.usage.input = 0;
|
|
24
|
+
output.usage.output = 0;
|
|
25
|
+
output.usage.cacheRead = 0;
|
|
26
|
+
output.usage.cacheWrite = 0;
|
|
27
|
+
}
|
|
28
|
+
if (usage.input_tokens != null) output.usage.input = usage.input_tokens;
|
|
29
|
+
if (usage.output_tokens != null) output.usage.output = usage.output_tokens;
|
|
30
|
+
if (usage.cache_read_input_tokens != null) output.usage.cacheRead = usage.cache_read_input_tokens;
|
|
31
|
+
if (usage.cache_creation_input_tokens != null) output.usage.cacheWrite = usage.cache_creation_input_tokens;
|
|
32
|
+
output.usage.totalTokens = output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** SDK raw stream_event (the Anthropic SSE protocol, re-exported) → pi assistant-message events. */
|
|
36
|
+
export function applyStreamEvent(state, event, parseStreamingJson) {
|
|
37
|
+
const { output, blocks, stream } = state;
|
|
38
|
+
const openBlock = (block, kind) => {
|
|
39
|
+
blocks.push(block);
|
|
40
|
+
output.content.push(block);
|
|
41
|
+
stream.push({ type: `${kind}_start`, contentIndex: output.content.length - 1, partial: output });
|
|
42
|
+
};
|
|
43
|
+
if (event.type === "message_start") {
|
|
44
|
+
output.responseId ??= event.message?.id || undefined;
|
|
45
|
+
updateUsage(state.model, output, event.message?.usage, { reset: true });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (event.type === "content_block_start") {
|
|
49
|
+
const block = event.content_block;
|
|
50
|
+
if (block?.type === "text") openBlock({ type: "text", text: "", index: event.index }, "text");
|
|
51
|
+
else if (block?.type === "thinking") openBlock({ type: "thinking", thinking: "", thinkingSignature: "", index: event.index }, "thinking");
|
|
52
|
+
else if (block?.type === "tool_use") {
|
|
53
|
+
openBlock({
|
|
54
|
+
type: "toolCall",
|
|
55
|
+
id: block.id,
|
|
56
|
+
name: mapSdkToolNameToPi(block.name),
|
|
57
|
+
arguments: asRecord(block.input),
|
|
58
|
+
partialJson: "",
|
|
59
|
+
index: event.index,
|
|
60
|
+
}, "toolcall");
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (event.type === "content_block_delta") {
|
|
65
|
+
const contentIndex = blocks.findIndex((candidate) => candidate.index === event.index);
|
|
66
|
+
const block = blocks[contentIndex];
|
|
67
|
+
const delta = event.delta;
|
|
68
|
+
if (delta?.type === "text_delta" && block?.type === "text") {
|
|
69
|
+
block.text += delta.text;
|
|
70
|
+
stream.push({ type: "text_delta", contentIndex, delta: delta.text, partial: output });
|
|
71
|
+
} else if (delta?.type === "thinking_delta" && block?.type === "thinking") {
|
|
72
|
+
block.thinking += delta.thinking;
|
|
73
|
+
stream.push({ type: "thinking_delta", contentIndex, delta: delta.thinking, partial: output });
|
|
74
|
+
} else if (delta?.type === "signature_delta" && block?.type === "thinking") {
|
|
75
|
+
block.thinkingSignature += delta.signature;
|
|
76
|
+
} else if (delta?.type === "input_json_delta" && block?.type === "toolCall") {
|
|
77
|
+
block.partialJson = `${block.partialJson ?? ""}${delta.partial_json}`;
|
|
78
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
79
|
+
stream.push({ type: "toolcall_delta", contentIndex, delta: delta.partial_json, partial: output });
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.type === "content_block_stop") {
|
|
84
|
+
const contentIndex = blocks.findIndex((candidate) => candidate.index === event.index);
|
|
85
|
+
const block = blocks[contentIndex];
|
|
86
|
+
if (block?.type === "text") stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
|
|
87
|
+
else if (block?.type === "thinking") stream.push({ type: "thinking_end", contentIndex, content: block.thinking, partial: output });
|
|
88
|
+
else if (block?.type === "toolCall") {
|
|
89
|
+
block.arguments = parseStreamingJson(block.partialJson);
|
|
90
|
+
delete block.partialJson;
|
|
91
|
+
stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
|
|
92
|
+
}
|
|
93
|
+
if (block) delete block.index;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (event.type === "message_delta") {
|
|
97
|
+
if (event.delta?.stop_reason) {
|
|
98
|
+
output.rawStopReason = event.delta.stop_reason;
|
|
99
|
+
output.stopReason = mapStopReason(event.delta.stop_reason);
|
|
100
|
+
}
|
|
101
|
+
updateUsage(state.model, output, event.usage);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function emptyOutput(model) {
|
|
106
|
+
return {
|
|
107
|
+
role: "assistant",
|
|
108
|
+
content: [],
|
|
109
|
+
api: model.api,
|
|
110
|
+
provider: model.provider,
|
|
111
|
+
model: model.id,
|
|
112
|
+
usage: {
|
|
113
|
+
contextUsage: null,
|
|
114
|
+
input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0,
|
|
115
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
116
|
+
},
|
|
117
|
+
stopReason: "stop",
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// The anthropic-subscription lane is one-shot: the SDK query gets no live conversation, so pi's
|
|
2
|
+
// message history is replayed into a single user message as labelled text, followed by an
|
|
3
|
+
// instruction to answer only the final user message. Tool calls stay host-side — a historical
|
|
4
|
+
// tool call or result is described, never re-executed by Claude Code.
|
|
5
|
+
const IMAGE_MEDIA_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
6
|
+
|
|
7
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
|
|
9
|
+
function appendEntry(blocks, entry) {
|
|
10
|
+
if (typeof entry === "string") {
|
|
11
|
+
if (entry.length > 0) blocks.push({ type: "text", text: entry });
|
|
12
|
+
return entry.trim().length > 0;
|
|
13
|
+
}
|
|
14
|
+
if (!isRecord(entry)) {
|
|
15
|
+
blocks.push({ type: "text", text: "[unsupported content block omitted]" });
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
if (entry.type === "text" && typeof entry.text === "string") {
|
|
19
|
+
blocks.push({ type: "text", text: entry.text });
|
|
20
|
+
return entry.text.trim().length > 0;
|
|
21
|
+
}
|
|
22
|
+
if (entry.type === "image" && typeof entry.data === "string" && IMAGE_MEDIA_TYPES.includes(entry.mimeType)) {
|
|
23
|
+
blocks.push({ type: "image", source: { type: "base64", media_type: entry.mimeType, data: entry.data } });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (entry.type === "image") {
|
|
27
|
+
blocks.push({ type: "text", text: `[image block omitted: unsupported media type ${entry.mimeType ?? "unknown"}]` });
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
blocks.push({ type: "text", text: `[unsupported content block omitted: ${entry.type}]` });
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** pi content (string or block array) → SDK content blocks appended to `blocks`; true when anything visible landed. */
|
|
35
|
+
function appendSdkContentBlocks(blocks, content) {
|
|
36
|
+
if (typeof content === "string") return appendEntry(blocks, content);
|
|
37
|
+
if (!Array.isArray(content)) return false;
|
|
38
|
+
let visible = false;
|
|
39
|
+
for (const entry of content) visible = appendEntry(blocks, entry) || visible;
|
|
40
|
+
return visible;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function contentToText(content) {
|
|
44
|
+
if (typeof content === "string") return content;
|
|
45
|
+
if (!Array.isArray(content)) return "";
|
|
46
|
+
return content
|
|
47
|
+
.map((block) => {
|
|
48
|
+
if (block.type === "text") return block.text;
|
|
49
|
+
if (block.type === "thinking") return block.thinking;
|
|
50
|
+
if (block.type === "toolCall") return `Historical tool call (non-executable): ${block.name} args=${JSON.stringify(block.arguments)}`;
|
|
51
|
+
return `[${block.type}]`;
|
|
52
|
+
})
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const CONVERSATION_HISTORY_CLOSER = "\n</conversation_history>";
|
|
58
|
+
export const RESPOND_TO_USER_INSTRUCTION = 'The above is the conversation history so far, provided as context. Respond as the assistant to the user message below only. Never emit "USER:" or "ASSISTANT:" labels or continue the transcript.';
|
|
59
|
+
/** A replay that ends with tool results (a fallback mid-turn) has no new user message; the last entries are the model's own tool outputs, not user input. */
|
|
60
|
+
export const CONTINUE_TURN_INSTRUCTION = 'The above is the conversation history so far, provided as context. There is no new user message: the last entries are results of tool calls you made in your current turn, not anything the user sent. Continue that turn as the assistant. Never emit "USER:" or "ASSISTANT:" labels or continue the transcript.';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* pi's context.messages → one SDK user message of content blocks. History is labelled text with a
|
|
64
|
+
* closing tag, then the instruction, then the final user message's own blocks (images included).
|
|
65
|
+
*/
|
|
66
|
+
export function buildPromptBlocks(context) {
|
|
67
|
+
const blocks = [];
|
|
68
|
+
const pushText = (text) => blocks.push({ type: "text", text });
|
|
69
|
+
// the system messages carry the prompt and tools, which reach the SDK through its own options
|
|
70
|
+
const messages = (context.messages ?? []).filter((message) => message.role !== "system");
|
|
71
|
+
const finalMessage = messages.at(-1);
|
|
72
|
+
const finalUserMessage = finalMessage?.role === "user" ? finalMessage : undefined;
|
|
73
|
+
const history = finalUserMessage ? messages.slice(0, -1) : messages;
|
|
74
|
+
const continuesTurn = !finalUserMessage && history.at(-1)?.role === "toolResult";
|
|
75
|
+
if (history.length > 0) {
|
|
76
|
+
pushText("<conversation_history>\n");
|
|
77
|
+
let hasPreviousTurn = false;
|
|
78
|
+
const pushPrefix = (label) => {
|
|
79
|
+
pushText(`${hasPreviousTurn ? "\n\n" : ""}${label}\n`);
|
|
80
|
+
hasPreviousTurn = true;
|
|
81
|
+
};
|
|
82
|
+
for (const message of history) {
|
|
83
|
+
if (message.role === "user") {
|
|
84
|
+
pushPrefix("USER:");
|
|
85
|
+
if (!appendSdkContentBlocks(blocks, message.content)) pushText("(see attached image)");
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (message.role === "assistant") {
|
|
89
|
+
pushPrefix("ASSISTANT:");
|
|
90
|
+
const text = contentToText(message.content);
|
|
91
|
+
if (text.length > 0) pushText(text);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (message.role === "toolResult") {
|
|
95
|
+
pushPrefix(`TOOL RESULT (historical ${message.toolName}, id=${message.toolCallId}):`);
|
|
96
|
+
if (!appendSdkContentBlocks(blocks, message.content)) pushText("(see attached image)");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
pushText(CONVERSATION_HISTORY_CLOSER);
|
|
100
|
+
}
|
|
101
|
+
pushText(continuesTurn ? CONTINUE_TURN_INSTRUCTION : RESPOND_TO_USER_INSTRUCTION);
|
|
102
|
+
if (finalUserMessage && !appendSdkContentBlocks(blocks, finalUserMessage.content)) pushText("(see attached image)");
|
|
103
|
+
return blocks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The SDK's stream-json prompt shape: one user message, delivered as an async iterable. */
|
|
107
|
+
export function buildPromptStream(promptBlocks) {
|
|
108
|
+
return (async function* () {
|
|
109
|
+
yield {
|
|
110
|
+
type: "user",
|
|
111
|
+
message: { role: "user", content: promptBlocks },
|
|
112
|
+
parent_tool_use_id: null,
|
|
113
|
+
session_id: "prompt",
|
|
114
|
+
};
|
|
115
|
+
})();
|
|
116
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// The subscription lane's stream body: spawns the official Claude Code binary through the Claude
|
|
2
|
+
// Agent SDK's `query()`, hands the OAuth access token over as CLAUDE_CODE_OAUTH_TOKEN, replays
|
|
3
|
+
// pi's message history into a single prompt (anthropic-subscription-prompt.mjs), and translates
|
|
4
|
+
// the SDK's stream events back into pi assistant-message events (anthropic-subscription-events.mjs).
|
|
5
|
+
// Tools stay host-side: built-in tools off, host tools on an SDK MCP server that always denies, so
|
|
6
|
+
// every tool call returns to CozyClay's harness and the result comes back in the next prompt.
|
|
7
|
+
|
|
8
|
+
import { resolveClaudeCodeExecutable } from "./claude-code-executable.mjs";
|
|
9
|
+
import { buildPromptBlocks, buildPromptStream } from "./anthropic-subscription-prompt.mjs";
|
|
10
|
+
import { applyStreamEvent, emptyOutput, mapStopReason, updateUsage } from "./anthropic-subscription-events.mjs";
|
|
11
|
+
const TOOL_EXECUTION_DENIED_MESSAGE = "Not an error: this is the normal host handoff, so ignore the 'hook error' label. " +
|
|
12
|
+
"CozyClay runs this tool on the host and returns its result as the next user message. " +
|
|
13
|
+
"Wait for that result, and do not mention this message to the user.";
|
|
14
|
+
|
|
15
|
+
// Environment the SDK subprocess must not inherit: a stored Anthropic key or gateway would make
|
|
16
|
+
// Claude Code spend it instead of the subscription token we inject.
|
|
17
|
+
function childEnvironment(parent, accessToken) {
|
|
18
|
+
const {
|
|
19
|
+
ANTHROPIC_API_KEY: _apiKey, ANTHROPIC_AUTH_TOKEN: _authToken, ANTHROPIC_BASE_URL: _baseUrl,
|
|
20
|
+
ANTHROPIC_CUSTOM_HEADERS: _customHeaders, CLAUDE_CODE_OAUTH_TOKEN: _oauthToken,
|
|
21
|
+
CLAUDE_CODE_USE_BEDROCK: _bedrock, CLAUDE_CODE_USE_FOUNDRY: _foundry,
|
|
22
|
+
CLAUDE_CODE_USE_GATEWAY: _gateway, CLAUDE_CODE_USE_VERTEX: _vertex,
|
|
23
|
+
...environment
|
|
24
|
+
} = parent;
|
|
25
|
+
for (const name of Object.keys(environment)) {
|
|
26
|
+
if (/^CLAUDE_CODE_OAUTH_TOKEN_\d+$/.test(name)) delete environment[name];
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...environment,
|
|
30
|
+
CLAUDE_CODE_OAUTH_TOKEN: accessToken,
|
|
31
|
+
// Claude Code's per-turn budget reminder rewrites the prompt every turn; CozyClay's harness
|
|
32
|
+
// owns context budgeting. Native auto-compact stays off: pi owns the transcript.
|
|
33
|
+
CLAUDE_CODE_TOTAL_TOKENS_REMINDER: "off",
|
|
34
|
+
DISABLE_COMPACT: "1",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// pi thinking level → SDK effort. Adaptive-thinking models take { type: "adaptive" } plus an
|
|
39
|
+
// effort grade; the rest take a raw token budget.
|
|
40
|
+
const DEFAULT_THINKING_BUDGETS = { minimal: 2048, low: 8192, medium: 16384, high: 31999, max: 63999 };
|
|
41
|
+
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
42
|
+
function applyThinking(queryOptions, model, reasoning) {
|
|
43
|
+
if (!reasoning || reasoning === "off") return;
|
|
44
|
+
const adaptive = model.compat?.forceAdaptiveThinking === true;
|
|
45
|
+
if (adaptive) {
|
|
46
|
+
const mapped = model.thinkingLevelMap?.[reasoning];
|
|
47
|
+
const effort = EFFORT_LEVELS.has(mapped) ? mapped
|
|
48
|
+
: reasoning === "minimal" || reasoning === "low" ? "low"
|
|
49
|
+
: reasoning === "medium" ? "medium"
|
|
50
|
+
: reasoning === "high" ? "high"
|
|
51
|
+
: "max";
|
|
52
|
+
queryOptions.thinking = { type: "adaptive", display: "summarized" };
|
|
53
|
+
queryOptions.effort = effort;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const level = reasoning === "xhigh" ? "high" : reasoning;
|
|
57
|
+
const budget = DEFAULT_THINKING_BUDGETS[level];
|
|
58
|
+
if (budget) queryOptions.maxThinkingTokens = budget;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** JSON Schema (pi's TypeBox parameters) → the Zod raw shape the SDK's `tool()` wants. */
|
|
62
|
+
async function jsonSchemaToZodShape(schema) {
|
|
63
|
+
const { z } = await import("zod");
|
|
64
|
+
const convert = (node) => {
|
|
65
|
+
const s = node ?? {};
|
|
66
|
+
let converted;
|
|
67
|
+
if (s.const !== undefined) converted = s.const === null ? z.null() : z.literal(s.const);
|
|
68
|
+
else if (Array.isArray(s.enum) && s.enum.length === 1) converted = z.literal(s.enum[0]);
|
|
69
|
+
else if (Array.isArray(s.enum) && s.enum.length > 1) {
|
|
70
|
+
const [first, second, ...rest] = s.enum.map((value) => z.literal(value));
|
|
71
|
+
converted = z.union([first, second, ...rest]);
|
|
72
|
+
}
|
|
73
|
+
if (converted === undefined) {
|
|
74
|
+
const variants = s.anyOf ?? s.oneOf;
|
|
75
|
+
if (Array.isArray(variants) && variants.length > 0) {
|
|
76
|
+
const [first, second, ...rest] = variants.map(convert);
|
|
77
|
+
converted = variants.length === 1 ? first : z.union([first, second, ...rest]);
|
|
78
|
+
} else {
|
|
79
|
+
const type = Array.isArray(s.type) ? s.type[0] : s.type;
|
|
80
|
+
if (type === "string") converted = z.string();
|
|
81
|
+
else if (type === "number") converted = z.number();
|
|
82
|
+
else if (type === "integer") converted = z.number().int();
|
|
83
|
+
else if (type === "boolean") converted = z.boolean();
|
|
84
|
+
else if (type === "null") converted = z.null();
|
|
85
|
+
else if (type === "array") converted = z.array(s.items ? convert(s.items) : z.unknown());
|
|
86
|
+
else if (type === "object") converted = z.object(shape(s));
|
|
87
|
+
else converted = z.unknown();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return typeof s.description === "string" ? converted.describe(s.description) : converted;
|
|
91
|
+
};
|
|
92
|
+
const shape = (objectSchema) => {
|
|
93
|
+
const properties = objectSchema?.properties ?? {};
|
|
94
|
+
const required = new Set(objectSchema?.required ?? []);
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
97
|
+
const converted = convert(value);
|
|
98
|
+
result[key] = required.has(key) ? converted : converted.optional();
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
};
|
|
102
|
+
return shape(schema ?? {});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** CozyClay's pi tools → an SDK MCP server. The handler never runs: it is the deny backstop. */
|
|
106
|
+
async function buildCustomToolServers(sdk, piTools) {
|
|
107
|
+
if (!piTools?.length) return undefined;
|
|
108
|
+
const tools = [];
|
|
109
|
+
for (const piTool of piTools) {
|
|
110
|
+
const schema = await jsonSchemaToZodShape(piTool.parameters);
|
|
111
|
+
tools.push(sdk.tool(
|
|
112
|
+
piTool.name,
|
|
113
|
+
typeof piTool.description === "string" ? piTool.description : "",
|
|
114
|
+
schema,
|
|
115
|
+
async () => ({ content: [{ type: "text", text: TOOL_EXECUTION_DENIED_MESSAGE }], isError: true }),
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
return { "cozyclay-tools": sdk.createSdkMcpServer({ name: "cozyclay-tools", version: "1.0.0", tools }) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The provider's api. pi's harness calls `streamSimple`; options.apiKey is the OAuth access
|
|
122
|
+
* token pi resolved (and refreshed) for this request, options.signal aborts, options.reasoning
|
|
123
|
+
* is the pi thinking level. */
|
|
124
|
+
export function createSubscriptionStream({ loadSdk, resolveExecutable = resolveClaudeCodeExecutable, env = process.env, cwd = process.cwd() } = {}) {
|
|
125
|
+
const sdkPromise = () => (loadSdk ?? (() => import("@anthropic-ai/claude-agent-sdk")))();
|
|
126
|
+
return function streamAnthropicSubscription(model, context, options = {}) {
|
|
127
|
+
return (async () => {
|
|
128
|
+
// pi hands providers a TranscriptContext: the prompt and the tools live in its system messages.
|
|
129
|
+
const { createAssistantMessageEventStream, getCurrentSystemPrompt, getCurrentTools, parseStreamingJson } = await import("@earendil-works/pi-ai");
|
|
130
|
+
const stream = createAssistantMessageEventStream();
|
|
131
|
+
void (async () => {
|
|
132
|
+
const output = emptyOutput(model);
|
|
133
|
+
const blocks = [];
|
|
134
|
+
let sdkQuery;
|
|
135
|
+
let closed = false;
|
|
136
|
+
let wasAborted = false;
|
|
137
|
+
let started = false;
|
|
138
|
+
let sawStreamEvent = false;
|
|
139
|
+
const closeQuery = () => {
|
|
140
|
+
if (closed || !sdkQuery) return;
|
|
141
|
+
closed = true;
|
|
142
|
+
sdkQuery.close();
|
|
143
|
+
};
|
|
144
|
+
const onAbort = () => {
|
|
145
|
+
wasAborted = true;
|
|
146
|
+
if (!sdkQuery) return;
|
|
147
|
+
void sdkQuery.interrupt().catch(() => {}).finally(closeQuery);
|
|
148
|
+
};
|
|
149
|
+
if (options.signal?.aborted) onAbort();
|
|
150
|
+
else options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
151
|
+
try {
|
|
152
|
+
const accessToken = options.apiKey;
|
|
153
|
+
if (typeof accessToken !== "string" || !accessToken) {
|
|
154
|
+
throw new Error("No Anthropic subscription credential resolved; sign in with Claude from the agent panel first.");
|
|
155
|
+
}
|
|
156
|
+
const sdk = await sdkPromise();
|
|
157
|
+
const executable = resolveExecutable({ env });
|
|
158
|
+
const piTools = getCurrentTools(context.messages);
|
|
159
|
+
const mcpServers = await buildCustomToolServers(sdk, piTools);
|
|
160
|
+
const queryOptions = {
|
|
161
|
+
cwd,
|
|
162
|
+
model: model.id,
|
|
163
|
+
// No Claude Code built-in tools; host tools come from the MCP server and every
|
|
164
|
+
// execution is denied, so the tool call always returns to CozyClay's harness.
|
|
165
|
+
tools: [],
|
|
166
|
+
permissionMode: "dontAsk",
|
|
167
|
+
includePartialMessages: true,
|
|
168
|
+
canUseTool: async () => ({ behavior: "deny", message: TOOL_EXECUTION_DENIED_MESSAGE }),
|
|
169
|
+
hooks: {
|
|
170
|
+
PreToolUse: [{
|
|
171
|
+
matcher: ".*",
|
|
172
|
+
hooks: [async () => ({
|
|
173
|
+
continue: false,
|
|
174
|
+
hookSpecificOutput: {
|
|
175
|
+
hookEventName: "PreToolUse",
|
|
176
|
+
permissionDecision: "deny",
|
|
177
|
+
permissionDecisionReason: TOOL_EXECUTION_DENIED_MESSAGE,
|
|
178
|
+
},
|
|
179
|
+
})],
|
|
180
|
+
}],
|
|
181
|
+
},
|
|
182
|
+
systemPrompt: {
|
|
183
|
+
type: "preset",
|
|
184
|
+
preset: "claude_code",
|
|
185
|
+
append: getCurrentSystemPrompt(context.messages).trim() || undefined,
|
|
186
|
+
},
|
|
187
|
+
env: childEnvironment(env, accessToken),
|
|
188
|
+
pathToClaudeCodeExecutable: executable,
|
|
189
|
+
settingSources: [],
|
|
190
|
+
extraArgs: { "strict-mcp-config": null },
|
|
191
|
+
};
|
|
192
|
+
if (mcpServers) queryOptions.mcpServers = mcpServers;
|
|
193
|
+
applyThinking(queryOptions, model, options.reasoning);
|
|
194
|
+
const promptBlocks = buildPromptBlocks(context);
|
|
195
|
+
sdkQuery = sdk.query({ prompt: buildPromptStream(promptBlocks), options: queryOptions });
|
|
196
|
+
if (wasAborted) onAbort();
|
|
197
|
+
for await (const message of sdkQuery) {
|
|
198
|
+
if (!started) {
|
|
199
|
+
stream.push({ type: "start", partial: output });
|
|
200
|
+
started = true;
|
|
201
|
+
}
|
|
202
|
+
if (message.type === "stream_event") {
|
|
203
|
+
sawStreamEvent = true;
|
|
204
|
+
applyStreamEvent({ model, output, blocks, stream }, message.event, parseStreamingJson);
|
|
205
|
+
} else if (message.type === "assistant") {
|
|
206
|
+
output.responseId ??= message.message?.id || undefined;
|
|
207
|
+
// SDK assistant snapshots carry placeholder output counts; only a query that
|
|
208
|
+
// streamed nothing may adopt them.
|
|
209
|
+
if (!sawStreamEvent) updateUsage(model, output, message.message?.usage, { reset: true });
|
|
210
|
+
} else if (message.type === "result") {
|
|
211
|
+
if (message.subtype !== "success") {
|
|
212
|
+
throw new Error(`Claude Code turn failed (${message.subtype ?? "unknown"})${message.error ? `: ${message.error}` : ""}`);
|
|
213
|
+
}
|
|
214
|
+
if (message.usage) updateUsage(model, output, message.usage);
|
|
215
|
+
if (message.stop_reason != null && output.stopReason !== "toolUse") {
|
|
216
|
+
output.stopReason = mapStopReason(message.stop_reason);
|
|
217
|
+
}
|
|
218
|
+
if (!sawStreamEvent && typeof message.result === "string" && message.result) {
|
|
219
|
+
output.content.push({ type: "text", text: message.result });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const block of output.content) {
|
|
224
|
+
delete block.index;
|
|
225
|
+
delete block.partialJson;
|
|
226
|
+
}
|
|
227
|
+
if (wasAborted || options.signal?.aborted) {
|
|
228
|
+
output.stopReason = "aborted";
|
|
229
|
+
output.errorMessage = "Operation aborted";
|
|
230
|
+
stream.push({ type: "error", reason: "aborted", error: output });
|
|
231
|
+
} else {
|
|
232
|
+
stream.push({ type: "done", reason: output.stopReason, message: output });
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
for (const block of output.content) {
|
|
236
|
+
delete block.index;
|
|
237
|
+
delete block.partialJson;
|
|
238
|
+
}
|
|
239
|
+
output.stopReason = wasAborted || options.signal?.aborted ? "aborted" : "error";
|
|
240
|
+
output.errorMessage = error instanceof Error ? error.message : String(error);
|
|
241
|
+
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
242
|
+
} finally {
|
|
243
|
+
closeQuery();
|
|
244
|
+
stream.end();
|
|
245
|
+
}
|
|
246
|
+
})();
|
|
247
|
+
return stream;
|
|
248
|
+
})();
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The Anthropic subscription lane (#claude-subscription): Claude Pro/Max OAuth sign-in, with the
|
|
2
|
+
// actual API calls made by the official Claude Code binary through the Claude Agent SDK.
|
|
3
|
+
//
|
|
4
|
+
// Why the indirection: Anthropic ties a subscription OAuth token to Claude Code. A third-party
|
|
5
|
+
// client calling api.anthropic.com with it gets refused and risks the account; a request the
|
|
6
|
+
// official binary sends is the sanctioned path. So this provider's `api.stream` never touches the
|
|
7
|
+
// Anthropic API itself — the stream implementation lives in anthropic-subscription-stream.mjs.
|
|
8
|
+
//
|
|
9
|
+
// Auth itself is pi's machinery: the provider reuses pi-ai's bundled Anthropic OAuth flow
|
|
10
|
+
// (login / refresh / toAuth), so login, the five-minute refresh window and auth.json storage work
|
|
11
|
+
// exactly like any other pi provider. `toAuth` yields `{ apiKey: <access token> }`, which pi's
|
|
12
|
+
// ModelsImpl passes to `stream` as `options.apiKey` — that is the token the stream injects.
|
|
13
|
+
|
|
14
|
+
import { createSubscriptionStream } from "./anthropic-subscription-stream.mjs";
|
|
15
|
+
|
|
16
|
+
export const SUBSCRIPTION_PROVIDER_ID = "anthropic-subscription";
|
|
17
|
+
// named like the ChatGPT lane ("ChatGPT (OpenAI Codex)"): the subscription product first
|
|
18
|
+
export const SUBSCRIPTION_PROVIDER_NAME = "Claude (Anthropic)";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The provider. Claude's catalogue is pi-ai's own anthropic list with the provider id rebound, so
|
|
22
|
+
* costs, context windows and thinking levels stay in sync with the API-key lane; auth is pi-ai's
|
|
23
|
+
* bundled Anthropic OAuth flow untouched.
|
|
24
|
+
*/
|
|
25
|
+
export async function anthropicSubscriptionProvider() {
|
|
26
|
+
const [{ anthropicProvider }, { createProvider }] = await Promise.all([
|
|
27
|
+
import("@earendil-works/pi-ai/providers/anthropic"),
|
|
28
|
+
import("@earendil-works/pi-ai"),
|
|
29
|
+
]);
|
|
30
|
+
const base = anthropicProvider();
|
|
31
|
+
const streamImpl = createSubscriptionStream();
|
|
32
|
+
const models = base.getModels().map((model) => ({
|
|
33
|
+
...model,
|
|
34
|
+
provider: SUBSCRIPTION_PROVIDER_ID,
|
|
35
|
+
compat: model.compat
|
|
36
|
+
? {
|
|
37
|
+
...model.compat,
|
|
38
|
+
allowedFallbackModels: model.compat.allowedFallbackModels?.map((fallback) => ({ ...fallback, provider: SUBSCRIPTION_PROVIDER_ID })),
|
|
39
|
+
}
|
|
40
|
+
: model.compat,
|
|
41
|
+
}));
|
|
42
|
+
return createProvider({
|
|
43
|
+
id: SUBSCRIPTION_PROVIDER_ID,
|
|
44
|
+
name: SUBSCRIPTION_PROVIDER_NAME,
|
|
45
|
+
auth: {
|
|
46
|
+
oauth: {
|
|
47
|
+
name: SUBSCRIPTION_PROVIDER_NAME,
|
|
48
|
+
// same convention as the ChatGPT lane's "Sign in with ChatGPT (Plus/Pro)"
|
|
49
|
+
loginLabel: "Sign in with Claude (Pro/Max)",
|
|
50
|
+
isSubscription: true,
|
|
51
|
+
login: (interaction) => base.auth.oauth.login(interaction),
|
|
52
|
+
refresh: (credential, signal) => base.auth.oauth.refresh(credential, signal),
|
|
53
|
+
toAuth: (credential) => base.auth.oauth.toAuth(credential),
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
models,
|
|
57
|
+
api: { stream: streamImpl, streamSimple: streamImpl },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Both Anthropic lanes of the agent registry. The subscription lane is the provider above; the
|
|
63
|
+
* API-key lane is pi-ai's anthropic provider minus the OAuth login it also advertises: spending a
|
|
64
|
+
* subscription token against the API directly is the refused path, so the panel's only Claude
|
|
65
|
+
* sign-in stays the subscription lane, whose requests run through the official Claude Code binary.
|
|
66
|
+
*/
|
|
67
|
+
export async function anthropicLaneProvider(id) {
|
|
68
|
+
if (id === SUBSCRIPTION_PROVIDER_ID) return anthropicSubscriptionProvider();
|
|
69
|
+
const { anthropicProvider } = await import("@earendil-works/pi-ai/providers/anthropic");
|
|
70
|
+
const provider = anthropicProvider();
|
|
71
|
+
if (provider.auth?.oauth) provider.auth = { ...provider.auth, oauth: undefined };
|
|
72
|
+
return provider;
|
|
73
|
+
}
|