Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36292d3fbb | ||
|
|
1ae344d8a6 | ||
|
|
01c43b0b0c | ||
|
|
fc4aa9a683 | ||
|
|
c043e9767f | ||
|
|
8b48299d8f | ||
|
|
0f27cff247 | ||
|
|
d0455f2683 |
No files matched your search
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
## 2026.1.15 (unreleased)
|
||||
|
||||
- Agents: add CLI quick reference to the system prompt to avoid invented commands. (#953) — thanks @roshanasingh4.
|
||||
- Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4.
|
||||
|
||||
## 2026.1.14-1
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
- Logging: tolerate `EIO` from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06.
|
||||
- Sandbox: restore `docker.binds` config validation and preserve configured PATH for `docker exec`. (#873) — thanks @akonyer.
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
- Agents: preserve Antigravity Claude signatures and skip Gemini downgrades. (#959) — thanks @rdev.
|
||||
|
||||
#### macOS / Apps
|
||||
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
|
||||
|
||||
@@ -213,6 +213,34 @@ describe("runWithModelFallback", () => {
|
||||
expect(calls).toEqual([{ provider: "anthropic", model: "claude-opus-4-5" }]);
|
||||
});
|
||||
|
||||
it("defaults provider/model when missing (regression #946)", async () => {
|
||||
const cfg = makeCfg({
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "openai/gpt-4.1-mini",
|
||||
fallbacks: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const calls: Array<{ provider: string; model: string }> = [];
|
||||
|
||||
const result = await runWithModelFallback({
|
||||
cfg,
|
||||
provider: undefined as unknown as string,
|
||||
model: undefined as unknown as string,
|
||||
run: async (provider, model) => {
|
||||
calls.push({ provider, model });
|
||||
return "ok";
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.result).toBe("ok");
|
||||
expect(calls).toEqual([{ provider: "openai", model: "gpt-4.1-mini" }]);
|
||||
});
|
||||
|
||||
it("falls back on missing API key errors", async () => {
|
||||
const cfg = makeCfg();
|
||||
const run = vi
|
||||
|
||||
@@ -119,8 +119,6 @@ function resolveFallbackCandidates(params: {
|
||||
/** Optional explicit fallbacks list; when provided (even empty), replaces agents.defaults.model.fallbacks. */
|
||||
fallbacksOverride?: string[];
|
||||
}): ModelCandidate[] {
|
||||
const provider = params.provider.trim() || DEFAULT_PROVIDER;
|
||||
const model = params.model.trim() || DEFAULT_MODEL;
|
||||
const primary = params.cfg
|
||||
? resolveConfiguredModelRef({
|
||||
cfg: params.cfg,
|
||||
@@ -128,11 +126,15 @@ function resolveFallbackCandidates(params: {
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
})
|
||||
: null;
|
||||
const defaultProvider = primary?.provider ?? DEFAULT_PROVIDER;
|
||||
const defaultModel = primary?.model ?? DEFAULT_MODEL;
|
||||
const provider = String(params.provider ?? "").trim() || defaultProvider;
|
||||
const model = String(params.model ?? "").trim() || defaultModel;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg ?? {},
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultProvider,
|
||||
});
|
||||
const allowlist = buildAllowedModelKeys(params.cfg, DEFAULT_PROVIDER);
|
||||
const allowlist = buildAllowedModelKeys(params.cfg, defaultProvider);
|
||||
const seen = new Set<string>();
|
||||
const candidates: ModelCandidate[] = [];
|
||||
|
||||
@@ -160,7 +162,7 @@ function resolveFallbackCandidates(params: {
|
||||
for (const raw of modelFallbacks) {
|
||||
const resolved = resolveModelRefFromString({
|
||||
raw: String(raw ?? ""),
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultProvider,
|
||||
aliasIndex,
|
||||
});
|
||||
if (!resolved) continue;
|
||||
|
||||
@@ -8,6 +8,12 @@ export function isGoogleModelApi(api?: string | null): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
export function isAntigravityClaude(api?: string | null, modelId?: string): boolean {
|
||||
if (api !== "google-antigravity") return false;
|
||||
return modelId?.toLowerCase().includes("claude") ?? false;
|
||||
}
|
||||
|
||||
|
||||
export { sanitizeGoogleTurnOrdering };
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,7 +30,7 @@ function isEmptyAssistantErrorMessage(
|
||||
export async function sanitizeSessionMessagesImages(
|
||||
messages: AgentMessage[],
|
||||
label: string,
|
||||
options?: { sanitizeToolCallIds?: boolean; enforceToolCallLast?: boolean },
|
||||
options?: { sanitizeToolCallIds?: boolean; enforceToolCallLast?: boolean; preserveSignatures?: boolean },
|
||||
): Promise<AgentMessage[]> {
|
||||
// We sanitize historical session messages because Anthropic can reject a request
|
||||
// if the transcript contains oversized base64 images (see MAX_IMAGE_DIMENSION_PX).
|
||||
@@ -76,7 +76,10 @@ export async function sanitizeSessionMessagesImages(
|
||||
}
|
||||
const content = assistantMsg.content;
|
||||
if (Array.isArray(content)) {
|
||||
const strippedContent = stripThoughtSignatures(content);
|
||||
const strippedContent = options?.preserveSignatures
|
||||
? content // Keep signatures for Antigravity Claude
|
||||
: stripThoughtSignatures(content); // Strip for Gemini
|
||||
|
||||
const filteredContent = strippedContent.filter((block) => {
|
||||
if (!block || typeof block !== "object") return true;
|
||||
const rec = block as { type?: unknown; text?: unknown };
|
||||
|
||||
@@ -59,6 +59,34 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
expect(assistant.content?.[0]?.thinkingSignature).toBe("sig");
|
||||
});
|
||||
|
||||
it("keeps unsigned thinking blocks for Antigravity Claude", async () => {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const input = [
|
||||
{
|
||||
role: "user",
|
||||
content: "hi",
|
||||
},
|
||||
{
|
||||
role: "assistant",
|
||||
content: [{ type: "thinking", thinking: "reasoning" }],
|
||||
},
|
||||
] satisfies AgentMessage[];
|
||||
|
||||
const out = await sanitizeSessionHistory({
|
||||
messages: input,
|
||||
modelApi: "google-antigravity",
|
||||
modelId: "anthropic/claude-3.5-sonnet",
|
||||
sessionManager,
|
||||
sessionId: "session:antigravity-claude",
|
||||
});
|
||||
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string; thinking?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("reasoning");
|
||||
});
|
||||
|
||||
it("preserves order when downgrading mixed assistant content", async () => {
|
||||
const sessionManager = SessionManager.inMemory();
|
||||
const input = [
|
||||
|
||||
@@ -302,6 +302,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||
const prior = await sanitizeSessionHistory({
|
||||
messages: session.messages,
|
||||
modelApi: model.api,
|
||||
modelId,
|
||||
sessionManager,
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { sanitizeToolUseResultPairing } from "../session-transcript-repair.js";
|
||||
import { log } from "./logger.js";
|
||||
import { describeUnknownError } from "./utils.js";
|
||||
import { isAntigravityClaude } from "../pi-embedded-helpers/google.js";
|
||||
|
||||
const GOOGLE_TURN_ORDERING_CUSTOM_TYPE = "google-turn-ordering-bootstrap";
|
||||
const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
|
||||
@@ -152,19 +153,23 @@ export function applyGoogleTurnOrderingFix(params: {
|
||||
export async function sanitizeSessionHistory(params: {
|
||||
messages: AgentMessage[];
|
||||
modelApi?: string | null;
|
||||
modelId?: string;
|
||||
sessionManager: SessionManager;
|
||||
sessionId: string;
|
||||
}): Promise<AgentMessage[]> {
|
||||
const isAntigravityClaudeModel = isAntigravityClaude(params.modelApi, params.modelId);
|
||||
const sanitizedImages = await sanitizeSessionMessagesImages(params.messages, "session:history", {
|
||||
sanitizeToolCallIds: shouldSanitizeToolCallIds(params.modelApi),
|
||||
enforceToolCallLast: params.modelApi === "anthropic-messages",
|
||||
preserveSignatures: params.modelApi === "google-antigravity" && isAntigravityClaudeModel,
|
||||
});
|
||||
const repairedTools = sanitizeToolUseResultPairing(sanitizedImages);
|
||||
const shouldDowngradeGemini = isGoogleModelApi(params.modelApi) && !isAntigravityClaudeModel;
|
||||
// Gemini rejects unsigned thinking blocks; downgrade them before send to avoid INVALID_ARGUMENT.
|
||||
const downgradedThinking = isGoogleModelApi(params.modelApi)
|
||||
const downgradedThinking = shouldDowngradeGemini
|
||||
? downgradeGeminiThinkingBlocks(repairedTools)
|
||||
: repairedTools;
|
||||
const downgraded = isGoogleModelApi(params.modelApi)
|
||||
const downgraded = shouldDowngradeGemini
|
||||
? downgradeGeminiHistory(downgradedThinking)
|
||||
: downgradedThinking;
|
||||
|
||||
|
||||
@@ -303,6 +303,7 @@ export async function runEmbeddedAttempt(
|
||||
const prior = await sanitizeSessionHistory({
|
||||
messages: activeSession.messages,
|
||||
modelApi: params.model.api,
|
||||
modelId: params.modelId,
|
||||
sessionManager,
|
||||
sessionId: params.sessionId,
|
||||
});
|
||||
|
||||
@@ -4,8 +4,14 @@ export type CommandArgsFormatter = (values: CommandArgValues) => string | undefi
|
||||
|
||||
function normalizeArgValue(value: unknown): string | undefined {
|
||||
if (value == null) return undefined;
|
||||
const text = typeof value === "string" ? value.trim() : String(value).trim();
|
||||
return text ? text : undefined;
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const formatConfigArgs: CommandArgsFormatter = (values) => {
|
||||
|
||||
@@ -137,9 +137,13 @@ function formatPositionalArgs(
|
||||
for (const definition of definitions) {
|
||||
const value = values[definition.name];
|
||||
if (value == null) continue;
|
||||
const rendered = typeof value === "string" ? value.trim() : String(value);
|
||||
if (!rendered) continue;
|
||||
parts.push(rendered);
|
||||
if (typeof value === "string") {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) continue;
|
||||
parts.push(trimmed);
|
||||
} else if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
parts.push(String(value));
|
||||
}
|
||||
if (definition.captureRemaining) break;
|
||||
}
|
||||
return parts.length > 0 ? parts.join(" ") : undefined;
|
||||
|
||||
@@ -78,11 +78,32 @@ export type TemplateContext = MsgContext & {
|
||||
IsNewSession?: string;
|
||||
};
|
||||
|
||||
function formatTemplateValue(value: unknown): string {
|
||||
if (value == null) return "";
|
||||
if (typeof value === "string") return value;
|
||||
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.flatMap((entry) => {
|
||||
if (entry == null) return [];
|
||||
if (typeof entry === "string") return [entry];
|
||||
if (typeof entry === "number" || typeof entry === "boolean" || typeof entry === "bigint") {
|
||||
return [String(entry)];
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.join(",");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Simple {{Placeholder}} interpolation using inbound message context.
|
||||
export function applyTemplate(str: string | undefined, ctx: TemplateContext) {
|
||||
if (!str) return "";
|
||||
return str.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
|
||||
const value = ctx[key as keyof TemplateContext];
|
||||
return value == null ? "" : String(value);
|
||||
return formatTemplateValue(value);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user