Compare commits
No commits in common. "fix/backoff-reset" and "master" have entirely different histories.
fix/backof
...
master
8 changed files with 50 additions and 109 deletions
|
|
@ -1,57 +0,0 @@
|
||||||
/**
|
|
||||||
* #3223-generalised: Antigravity answers a system prompt that advertises a rival
|
|
||||||
* agent with 429 RESOURCE_EXHAUSTED. Upstream stripped one Zed literal; every
|
|
||||||
* other client wording still tripped it. This pins the shapes we must strip.
|
|
||||||
*
|
|
||||||
* Run: node open-sse/executors/antigravity-competitor-strip.check.mjs
|
|
||||||
*/
|
|
||||||
import assert from 'node:assert/strict';
|
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import { dirname, resolve } from 'node:path';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import vm from 'node:vm';
|
|
||||||
|
|
||||||
const here = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const src = readFileSync(
|
|
||||||
process.env.AG_SOURCE || resolve(here, 'antigravity.js'),
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Lift the strip block out of buildRequest and run it against a fake request.
|
|
||||||
const start = src.indexOf('if (requestWithoutTools.systemInstruction?.parts)');
|
|
||||||
assert.ok(start >= 0, 'competitor-strip block must exist');
|
|
||||||
const end = src.indexOf('\n }\n', src.indexOf('for (const part', start)) + 6;
|
|
||||||
const block = src.slice(start, end);
|
|
||||||
|
|
||||||
const strip = (text) => {
|
|
||||||
const requestWithoutTools = { systemInstruction: { parts: [{ text }] } };
|
|
||||||
vm.runInNewContext(block, { requestWithoutTools });
|
|
||||||
return requestWithoutTools.systemInstruction.parts[0].text;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The literal upstream already handled — must keep working.
|
|
||||||
assert.ok(
|
|
||||||
!strip("You are a Claude agent, built on Anthropic's Claude Agent SDK. Be terse.")
|
|
||||||
.includes('Claude'),
|
|
||||||
'Zed wording must still be stripped'
|
|
||||||
);
|
|
||||||
|
|
||||||
// The wordings upstream missed.
|
|
||||||
for (const prompt of [
|
|
||||||
"You are Claude Code, Anthropic's official CLI for Claude. Help the user.",
|
|
||||||
'You are Claude, made by Anthropic!',
|
|
||||||
'you are a claude agent. lowercase should not save it.',
|
|
||||||
]) {
|
|
||||||
const out = strip(prompt);
|
|
||||||
assert.ok(!/claude/i.test(out), `must strip competitor branding from: ${prompt}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must not eat an innocent prompt.
|
|
||||||
const benign = 'You are a helpful assistant. Answer in French.';
|
|
||||||
assert.equal(strip(benign), benign, 'benign prompts must pass through untouched');
|
|
||||||
|
|
||||||
// Non-string parts must not throw.
|
|
||||||
const req = { systemInstruction: { parts: [{ inlineData: {} }] } };
|
|
||||||
vm.runInNewContext(block, { requestWithoutTools: req });
|
|
||||||
|
|
||||||
console.log('antigravity-competitor-strip: all passed');
|
|
||||||
|
|
@ -246,27 +246,17 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||||
const { tools: _originalTools, toolConfig: _originalToolConfig, ...requestWithoutTools } = body.request || {};
|
const { tools: _originalTools, toolConfig: _originalToolConfig, ...requestWithoutTools } = body.request || {};
|
||||||
stripBlacklisted(requestWithoutTools);
|
stripBlacklisted(requestWithoutTools);
|
||||||
|
|
||||||
// Rewrite competitive system prompts before they reach Antigravity. Google
|
// Rewrite competitive system prompts (e.g. Zed IDE's Claude prompt) to prevent Antigravity from
|
||||||
// inspects systemInstruction and answers a prompt that advertises a rival
|
// flagging the request and immediately blocking it with a 429 Quota Exhausted response.
|
||||||
// agent with 429 RESOURCE_EXHAUSTED — a content refusal wearing a quota
|
|
||||||
// error's clothes, which is why it survives every backoff and endpoint fix.
|
|
||||||
// Upstream PR #3223 stripped ONE literal (Zed's "You are a Claude agent,
|
|
||||||
// built on Anthropic's Claude Agent SDK."), so every other client — Claude
|
|
||||||
// Code, Hermes, Cline — still trips it. Match the shape instead.
|
|
||||||
// ponytail: regex over a prompt-classifier; widen the alternation if a new
|
|
||||||
// client wording slips through.
|
|
||||||
if (requestWithoutTools.systemInstruction?.parts) {
|
if (requestWithoutTools.systemInstruction?.parts) {
|
||||||
|
const oldText = "You are a Claude agent, built on Anthropic's Claude Agent SDK.";
|
||||||
for (const part of requestWithoutTools.systemInstruction.parts) {
|
for (const part of requestWithoutTools.systemInstruction.parts) {
|
||||||
if (typeof part.text !== "string") continue;
|
if (typeof part.text === "string" && part.text.includes(oldText)) {
|
||||||
part.text = part.text
|
part.text = part.text.split(oldText).join("");
|
||||||
// "You are Claude Code, Anthropic's official CLI for Claude." /
|
|
||||||
// "You are a Claude agent, built on Anthropic's Claude Agent SDK."
|
|
||||||
.replace(/You are (?:a |an )?Claude\b[^.!?]*[.!?]\s*/gi, "")
|
|
||||||
// Residual vendor branding in the same sentence position.
|
|
||||||
.replace(/\b(?:Anthropic's|Anthropic)\s+(?:official\s+)?(?:CLI|Claude Agent SDK)\b[^.!?]*[.!?]\s*/gi, "")
|
|
||||||
.trimStart();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const generationConfig = { ...(requestWithoutTools.generationConfig || {}) };
|
const generationConfig = { ...(requestWithoutTools.generationConfig || {}) };
|
||||||
if (generationConfig.maxOutputTokens > MAX_ANTIGRAVITY_OUTPUT_TOKENS) {
|
if (generationConfig.maxOutputTokens > MAX_ANTIGRAVITY_OUTPUT_TOKENS) {
|
||||||
generationConfig.maxOutputTokens = MAX_ANTIGRAVITY_OUTPUT_TOKENS;
|
generationConfig.maxOutputTokens = MAX_ANTIGRAVITY_OUTPUT_TOKENS;
|
||||||
|
|
|
||||||
|
|
@ -185,13 +185,9 @@ export function resetAccountState(account) {
|
||||||
if (!account) return account;
|
if (!account) return account;
|
||||||
return {
|
return {
|
||||||
...account,
|
...account,
|
||||||
...buildClearModelLocksUpdate(account),
|
|
||||||
rateLimitedUntil: null,
|
rateLimitedUntil: null,
|
||||||
backoffLevel: 0,
|
backoffLevel: 0,
|
||||||
testStatus: "active",
|
|
||||||
lastError: null,
|
lastError: null,
|
||||||
errorCode: null,
|
|
||||||
lastErrorAt: null,
|
|
||||||
status: "active"
|
status: "active"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,14 +189,13 @@ export async function createProviderConnection(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Critical: OAuth refresh token race — atomic merge inside transaction
|
// Critical: OAuth refresh token race — atomic merge inside transaction
|
||||||
export async function updateProviderConnection(id, update) {
|
export async function updateProviderConnection(id, data) {
|
||||||
const db = await getAdapter();
|
const db = await getAdapter();
|
||||||
let result;
|
let result;
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
const row = db.get(`SELECT * FROM providerConnections WHERE id = ?`, [id]);
|
const row = db.get(`SELECT * FROM providerConnections WHERE id = ?`, [id]);
|
||||||
if (!row) { result = null; return; }
|
if (!row) { result = null; return; }
|
||||||
const existing = rowToConn(row);
|
const existing = rowToConn(row);
|
||||||
const data = typeof update === "function" ? update(existing) : update;
|
|
||||||
const merged = { ...existing, ...data, updatedAt: new Date().toISOString() };
|
const merged = { ...existing, ...data, updatedAt: new Date().toISOString() };
|
||||||
upsert(db, merged);
|
upsert(db, merged);
|
||||||
if (data.priority !== undefined) reorderInTx(db, existing.provider);
|
if (data.priority !== undefined) reorderInTx(db, existing.provider);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
extractApiKey, isValidApiKey,
|
extractApiKey, isValidApiKey,
|
||||||
getProviderCredentials, markAccountUnavailable, clearAccountError,
|
getProviderCredentials, markAccountUnavailable,
|
||||||
} from "../services/auth.js";
|
} from "../services/auth.js";
|
||||||
import { getSettings } from "@/lib/localDb";
|
import { getSettings } from "@/lib/localDb";
|
||||||
import { getModelInfo } from "../services/model.js";
|
import { getModelInfo } from "../services/model.js";
|
||||||
|
|
@ -74,10 +74,7 @@ export async function handleStt(request) {
|
||||||
|
|
||||||
const result = await handleSttCore({ provider, model, formData, credentials, sttConfig: AI_PROVIDERS[provider]?.sttConfig });
|
const result = await handleSttCore({ provider, model, formData, credentials, sttConfig: AI_PROVIDERS[provider]?.sttConfig });
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) return result.response;
|
||||||
await clearAccountError(credentials.connectionId, credentials, model);
|
|
||||||
return result.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model);
|
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model);
|
||||||
if (shouldFallback) {
|
if (shouldFallback) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import {
|
import {
|
||||||
extractApiKey, isValidApiKey,
|
extractApiKey, isValidApiKey,
|
||||||
getProviderCredentials, markAccountUnavailable, clearAccountError,
|
getProviderCredentials, markAccountUnavailable,
|
||||||
} from "../services/auth.js";
|
} from "../services/auth.js";
|
||||||
import { getSettings } from "@/lib/localDb";
|
import { getSettings } from "@/lib/localDb";
|
||||||
import { getModelInfo, getComboModels } from "../services/model.js";
|
import { getModelInfo, getComboModels } from "../services/model.js";
|
||||||
|
|
@ -101,10 +101,7 @@ async function handleSingleModelTts(body, modelStr, responseFormat, language, st
|
||||||
|
|
||||||
const result = await handleTtsCore({ provider, model, input: body.input, credentials, responseFormat, language, style });
|
const result = await handleTtsCore({ provider, model, input: body.input, credentials, responseFormat, language, style });
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) return result.response;
|
||||||
await clearAccountError(credentials.connectionId, credentials, model);
|
|
||||||
return result.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model);
|
const { shouldFallback } = await markAccountUnavailable(credentials.connectionId, result.status, result.error, provider, model);
|
||||||
if (shouldFallback) {
|
if (shouldFallback) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { getProviderConnections, validateApiKey, updateProviderConnection, getSettings, getProxyPools } from "@/lib/localDb";
|
import { getProviderConnections, validateApiKey, updateProviderConnection, getSettings, getProxyPools } from "@/lib/localDb";
|
||||||
import { resolveConnectionProxyConfig, pickProxyPoolId } from "@/lib/network/connectionProxy";
|
import { resolveConnectionProxyConfig, pickProxyPoolId } from "@/lib/network/connectionProxy";
|
||||||
import { formatRetryAfter, checkFallbackError, isModelLockActive, buildModelLockUpdate, getEarliestModelLockUntil, resetAccountState } from "open-sse/services/accountFallback.js";
|
import { formatRetryAfter, checkFallbackError, isModelLockActive, buildModelLockUpdate, getEarliestModelLockUntil } from "open-sse/services/accountFallback.js";
|
||||||
import { MAX_RATE_LIMIT_COOLDOWN_MS } from "open-sse/config/errorConfig.js";
|
import { MAX_RATE_LIMIT_COOLDOWN_MS } from "open-sse/config/errorConfig.js";
|
||||||
import { resolveProviderId, FREE_PROVIDERS } from "@/shared/constants/providers.js";
|
import { resolveProviderId, FREE_PROVIDERS } from "@/shared/constants/providers.js";
|
||||||
import * as log from "../utils/logger.js";
|
import * as log from "../utils/logger.js";
|
||||||
|
|
@ -274,8 +274,43 @@ export async function markAccountUnavailable(connectionId, status, errorText, pr
|
||||||
*/
|
*/
|
||||||
export async function clearAccountError(connectionId, currentConnection, model = null) {
|
export async function clearAccountError(connectionId, currentConnection, model = null) {
|
||||||
if (!connectionId || connectionId === "noauth") return;
|
if (!connectionId || connectionId === "noauth") return;
|
||||||
// Reset inside transaction so concurrent 429 writes cannot leave stale locks.
|
const conn = currentConnection._connection || currentConnection;
|
||||||
await updateProviderConnection(connectionId, resetAccountState);
|
const now = Date.now();
|
||||||
|
const allLockKeys = Object.keys(conn).filter(k => k.startsWith("modelLock_"));
|
||||||
|
|
||||||
|
if (!conn.testStatus && !conn.lastError && allLockKeys.length === 0) return;
|
||||||
|
|
||||||
|
// Keys to clear: current model's lock + all expired locks
|
||||||
|
const keysToClear = allLockKeys.filter(k => {
|
||||||
|
if (model && k === `modelLock_${model}`) return true; // succeeded model
|
||||||
|
if (model && k === "modelLock___all") return true; // account-level lock
|
||||||
|
const expiry = conn[k];
|
||||||
|
return expiry && new Date(expiry).getTime() <= now; // expired
|
||||||
|
});
|
||||||
|
|
||||||
|
if (keysToClear.length === 0 && conn.testStatus !== "unavailable" && !conn.lastError) return;
|
||||||
|
|
||||||
|
// Check if any active locks remain after clearing
|
||||||
|
const remainingActiveLocks = allLockKeys.filter(k => {
|
||||||
|
if (keysToClear.includes(k)) return false;
|
||||||
|
const expiry = conn[k];
|
||||||
|
return expiry && new Date(expiry).getTime() > now;
|
||||||
|
});
|
||||||
|
|
||||||
|
const clearObj = Object.fromEntries(keysToClear.map(k => [k, null]));
|
||||||
|
|
||||||
|
// Only reset error state if no active locks remain
|
||||||
|
if (remainingActiveLocks.length === 0) {
|
||||||
|
Object.assign(clearObj, {
|
||||||
|
testStatus: "active",
|
||||||
|
lastError: null,
|
||||||
|
errorCode: null,
|
||||||
|
lastErrorAt: null,
|
||||||
|
backoffLevel: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProviderConnection(connectionId, clearObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { applyErrorState, resetAccountState } from "../../open-sse/services/accountFallback.js";
|
|
||||||
|
|
||||||
describe("resetAccountState", () => {
|
|
||||||
it("clears every model lock and backoff after success", () => {
|
|
||||||
const reset = resetAccountState({ backoffLevel: 9, modelLock_alpha: "2099-01-01T00:00:00.000Z", modelLock___all: "2099-01-01T00:00:00.000Z", lastError: "429", errorCode: 429 });
|
|
||||||
expect(reset.backoffLevel).toBe(0);
|
|
||||||
expect(reset.modelLock_alpha).toBeNull();
|
|
||||||
expect(reset.modelLock___all).toBeNull();
|
|
||||||
expect(reset.lastError).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps 429 on error path ratcheting upward", () => {
|
|
||||||
expect(applyErrorState({ backoffLevel: 3 }, 429, "rate limited").backoffLevel).toBe(4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue