pi-roundtable-sandbox 0.7.14 → 0.7.15
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 +4 -0
- package/package.json +2 -2
- package/src/pi-model-input.ts +49 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.15
|
|
4
|
+
|
|
5
|
+
- The model broker forwards a mid-conversation `role: "system"` message, which Claude Code 2.1.284 sends with each turn's environment, instead of refusing it. Like every other part of the request it is rebuilt, not passed on: its content may be only text (a string, or `text` blocks with `cache_control`), it keeps `clear_at`, and its per-message `output_config.effort` is capped at the host-judged level as the top-level effort is. The guest already writes the top-level system prompt and every user turn, so this text gives it no more reach. A tool change inside one (`tool_addition`, `tool_removal`, `tool_definition`) is still refused, with a 400 worded as the API words it, so Claude Code resends its tools whole.
|
|
6
|
+
|
|
3
7
|
## 0.7.14
|
|
4
8
|
|
|
5
9
|
- A model request the broker refuses (an unknown role, a native tool, remote media, a remote schema) is answered with a 400 in the Anthropic API's error shape (`invalid_request_error`) instead of a 502, and logged as `sandbox upstream call failed`. Claude Code 2.1.284 (agent SDK 0.3.284) sends a mid-conversation `role: "system"` message when MCP tools arrive late; the broker refused it with a 502, which Claude Code retried about ten times over several minutes before the turn failed. The refusal of a role now reads `messages.N: Unexpected role "system": the input message role must be "user" or "assistant"`, which Claude Code recognizes: it resends without system messages for the rest of that conversation. The broker still never forwards a system message.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-roundtable-sandbox",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "Sealed guest channels and an allow-listed credential broker for pi-roundtable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"@biomejs/biome": "2.5.15",
|
|
53
53
|
"@types/bun": "1.4.2",
|
|
54
54
|
"discord.js": "14.27.0",
|
|
55
|
-
"pi-roundtable": "0.7.
|
|
55
|
+
"pi-roundtable": "0.7.15",
|
|
56
56
|
"typebox": "1.3.34",
|
|
57
57
|
"typescript": "7.0.2"
|
|
58
58
|
}
|
package/src/pi-model-input.ts
CHANGED
|
@@ -84,6 +84,28 @@ function blocks(raw: unknown, nested = false): unknown[] | string {
|
|
|
84
84
|
throw new Error("Native tools or remote media refused");
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A mid-conversation system message's content: text only. The guest already writes the top-level
|
|
89
|
+
* system prompt and every user turn, so its text here carries no more authority. Its tool changes
|
|
90
|
+
* are refused in the API's wording, so Claude Code resends the tools whole instead.
|
|
91
|
+
*/
|
|
92
|
+
function systemContent(raw: unknown, index: number): unknown[] | string {
|
|
93
|
+
if (typeof raw === "string") return raw;
|
|
94
|
+
if (!Array.isArray(raw) || raw.length > 256)
|
|
95
|
+
throw new Error("Invalid content");
|
|
96
|
+
return raw.map((value: unknown, block) => {
|
|
97
|
+
if (
|
|
98
|
+
isRecord(value) &&
|
|
99
|
+
value.type === "text" &&
|
|
100
|
+
typeof value.text === "string"
|
|
101
|
+
)
|
|
102
|
+
return { type: "text", text: value.text, ...cache(value) };
|
|
103
|
+
const type = isRecord(value) ? String(value.type) : typeof value;
|
|
104
|
+
throw new Error(
|
|
105
|
+
`messages.${index}.content.${block}: Input tag '${type}' found using 'type' does not match any of the expected tags: 'text'`,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
87
109
|
const EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const;
|
|
88
110
|
/** Highest effort and extended-thinking budget the host-judged level allows. */
|
|
89
111
|
const CEILINGS: Record<PiThinkingLevel, { effort: number; budget: number }> = {
|
|
@@ -104,15 +126,37 @@ export function piModelInput(
|
|
|
104
126
|
): Record<string, unknown> {
|
|
105
127
|
if (!Array.isArray(input.messages) || input.messages.length > 2000)
|
|
106
128
|
throw new Error("Invalid messages");
|
|
129
|
+
const ceiling = CEILINGS[thinking];
|
|
130
|
+
/** An effort the guest asks for, at most the host-judged level allows; -1 when none. */
|
|
131
|
+
const cappedEffort = (config: unknown): number => {
|
|
132
|
+
const asked = isRecord(config)
|
|
133
|
+
? EFFORTS.indexOf(config.effort as (typeof EFFORTS)[number])
|
|
134
|
+
: -1;
|
|
135
|
+
return asked >= 0 ? Math.min(asked, ceiling.effort) : -1;
|
|
136
|
+
};
|
|
107
137
|
const body: Record<string, unknown> = {
|
|
108
138
|
model,
|
|
109
139
|
messages: input.messages.map((message: unknown, index) => {
|
|
110
140
|
if (!isRecord(message)) throw new Error("Invalid message");
|
|
111
|
-
//
|
|
112
|
-
|
|
141
|
+
// Claude Code sends each turn's environment as a mid-conversation system message.
|
|
142
|
+
if (message.role === "system") {
|
|
143
|
+
const effort = cappedEffort(message.output_config);
|
|
144
|
+
return {
|
|
145
|
+
role: "system",
|
|
146
|
+
content: systemContent(message.content, index),
|
|
147
|
+
...(message.clear_at === "next_user_message" ||
|
|
148
|
+
message.clear_at === "never"
|
|
149
|
+
? { clear_at: message.clear_at }
|
|
150
|
+
: {}),
|
|
151
|
+
...(effort >= 0
|
|
152
|
+
? { output_config: { effort: EFFORTS[effort] } }
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Worded as the Anthropic API words it, so a client recognizes the refusal.
|
|
113
157
|
if (!["user", "assistant"].includes(String(message.role)))
|
|
114
158
|
throw new Error(
|
|
115
|
-
`messages.${index}: Unexpected role ${JSON.stringify(String(message.role))}: the input message role must be "user" or "
|
|
159
|
+
`messages.${index}: Unexpected role ${JSON.stringify(String(message.role))}: the input message role must be "user", "assistant" or "system"`,
|
|
116
160
|
);
|
|
117
161
|
return { role: message.role, content: blocks(message.content) };
|
|
118
162
|
}),
|
|
@@ -155,7 +199,6 @@ export function piModelInput(
|
|
|
155
199
|
: {}),
|
|
156
200
|
};
|
|
157
201
|
}
|
|
158
|
-
const ceiling = CEILINGS[thinking];
|
|
159
202
|
if (isRecord(input.thinking)) {
|
|
160
203
|
if (
|
|
161
204
|
input.thinking.type === "adaptive" ||
|
|
@@ -177,13 +220,11 @@ export function piModelInput(
|
|
|
177
220
|
};
|
|
178
221
|
else throw new Error("Invalid thinking");
|
|
179
222
|
}
|
|
180
|
-
const asked =
|
|
181
|
-
? EFFORTS.indexOf(input.output_config.effort as (typeof EFFORTS)[number])
|
|
182
|
-
: -1;
|
|
223
|
+
const asked = cappedEffort(input.output_config);
|
|
183
224
|
// Adaptive thinking without an explicit effort runs at the model's default, so the host caps it too.
|
|
184
225
|
const effort =
|
|
185
226
|
asked >= 0
|
|
186
|
-
?
|
|
227
|
+
? asked
|
|
187
228
|
: isRecord(body.thinking) &&
|
|
188
229
|
body.thinking.type === "adaptive" &&
|
|
189
230
|
ceiling.effort < 2
|