Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/gateway-waf-blocked-diagnostic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@open-codesign/shared': patch
'@open-codesign/desktop': patch
'@open-codesign/i18n': patch
---

Classify 403 generation responses that say requests were blocked as gateway or reverse-proxy blocks instead of invalid API keys.
24 changes: 24 additions & 0 deletions apps/desktop/src/renderer/src/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1390,6 +1390,30 @@ describe('applyGenerateError via sendPrompt', () => {
});
});

it('classifies 403 blocked generation errors as gateway WAF blocks', async () => {
const err = Object.assign(
new Error(
"Error invoking remote method 'codesign:v1:generate': CodesignError: 403 Your request was blocked.",
),
{
code: 'PROVIDER_ERROR',
upstream_provider: 'anthropic',
upstream_status: 403,
upstream_baseurl: 'https://relay.example.com/v1',
},
);

await runFailingGenerate(err);

const state = useCodesignStore.getState();
expect(state.toasts[0]?.description).toContain('gateway or reverse proxy blocked');
expect(state.toasts[0]?.description).not.toContain('API key invalid');
expect(state.reportableErrors[0]?.context).toMatchObject({
diagnostic_category: 'gateway-waf-blocked',
display_message: '403 Your request was blocked.',
});
});

it('cleans Electron IPC wrapper for toast/chat while preserving raw report context', async () => {
const rawMessage =
"Error invoking remote method 'codesign:v1:generate': CodesignError: 404 model 'models/gemini-2.5-pro' not found";
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,7 @@
"sslError": "SSL / certificate error (self-signed cert on relay?).",
"endpointNotFound": "The endpoint path exists in the Base URL but the provider did not expose this route.",
"gatewayIncompatible": "The gateway accepted the connection but does not implement this provider's API. Try switching wire (e.g. openai-chat).",
"gatewayWafBlocked": "The gateway or reverse proxy blocked the generation request before it reached the model. Test Connection can still pass because it only probes the /models endpoint.",
"openaiResponsesMisconfigured": "The endpoint rejected the request shape. The wire may be wrong — try switching to openai-chat.",
"unsupportedRole": "The endpoint rejected the role format sent to the model. This is usually a wire or model-policy mismatch.",
"reasoningPolicy": "The endpoint rejected reasoning metadata for this model. Lower the reasoning depth and try again.",
Expand Down Expand Up @@ -891,6 +892,7 @@
"disableReasoning": "Turn reasoning off",
"normalizeModelId": "Remove model prefix",
"checkReferenceUrl": "Check reference URL",
"gatewayWafBlocked": "Check proxy allowlist / headers",
"relayStreamingBug": "Upgrade the relay, switch wire to openai-chat, or use api.openai.com directly"
},
"applyFix": "Apply this fix",
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,7 @@
"corsError": "Error de CORS (no debería pasar en el proceso principal). Esto es un error.",
"sslError": "Error de SSL / certificado (¿certificado autofirmado en la retransmisión?).",
"gatewayIncompatible": "La pasarela aceptó la conexión pero no implementa la API de este proveedor. Intenta cambiar de protocolo (ej. openai-chat).",
"gatewayWafBlocked": "La pasarela o el proxy inverso bloqueó la generación antes de llegar al modelo. Test Connection puede pasar porque solo prueba el endpoint /models.",
"openaiResponsesMisconfigured": "El punto final rechazó la forma de la solicitud. El protocolo puede ser incorrecto — intenta cambiar a openai-chat.",
"relayStreamingBug": "La pasarela puede manejar mal los eventos SSE de la API OpenAI Responses (versiones antiguas de sub2api / claude2api / anyrouter cortan el flujo prematuramente).",
"serverError": "Error del servidor ascendente. Puede ser transitorio — intenta de nuevo.",
Expand All @@ -874,6 +875,7 @@
"reportBug": "Reportar este error",
"disableTls": "Desactivar verificación TLS",
"switchWire": "Cambiar protocolo en Configuración",
"gatewayWafBlocked": "Revisar allowlist / headers del proxy",
"relayStreamingBug": "Actualiza la retransmisión, cambia el protocolo a openai-chat o usa api.openai.com directamente"
},
"applyFix": "Aplicar esta corrección",
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@
"sslError": "Erro de SSL / certificado (certificado autoassinado no relay?).",
"endpointNotFound": "A URL base já contém um caminho de versão, mas o provedor não expôs esta rota.",
"gatewayIncompatible": "O gateway aceitou a conexão, mas não implementa a API deste provedor. Tente trocar o wire, por exemplo para openai-chat.",
"gatewayWafBlocked": "O gateway ou proxy reverso bloqueou a geração antes de ela chegar ao modelo. Test Connection ainda pode passar porque só verifica o endpoint /models.",
"openaiResponsesMisconfigured": "O endpoint rejeitou o formato da requisição. O wire pode estar errado; tente openai-chat.",
"unsupportedRole": "O endpoint rejeitou o formato de role enviado ao modelo. Normalmente é incompatibilidade de wire ou política do modelo.",
"reasoningPolicy": "O endpoint rejeitou metadados de reasoning para este modelo. Reduza a profundidade de reasoning e tente novamente.",
Expand Down Expand Up @@ -851,6 +852,7 @@
"disableReasoning": "Desativar reasoning",
"normalizeModelId": "Remover prefixo do modelo",
"checkReferenceUrl": "Verificar URL de referência",
"gatewayWafBlocked": "Verificar allowlist / headers do proxy",
"relayStreamingBug": "Atualize o relay, troque o wire para openai-chat ou use api.openai.com diretamente"
},
"applyFix": "Aplicar esta correção",
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@
"sslError": "SSL / 证书错误(中转服务使用了自签证书?)。",
"endpointNotFound": "Base URL 已包含版本路径,但 Provider 没有暴露这个接口路径。",
"gatewayIncompatible": "网关接受了连接但没有实现该 Provider 的 API。尝试切换 wire(例如改为 openai-chat)。",
"gatewayWafBlocked": "网关或反代在请求到达模型前拦截了生成请求。测试连接可能仍然通过,因为它只探测 /models 端点。",
"openaiResponsesMisconfigured": "端点拒绝了请求格式。wire 可能配错了——尝试切换到 openai-chat。",
"unsupportedRole": "端点拒绝了发送给模型的 role 格式,通常是 wire 或模型策略不兼容。",
"reasoningPolicy": "端点拒绝了该模型的 reasoning 元数据。请降低 reasoning 深度后重试。",
Expand Down Expand Up @@ -887,6 +888,7 @@
"disableReasoning": "关闭 reasoning",
"normalizeModelId": "移除模型前缀",
"checkReferenceUrl": "检查 Reference URL",
"gatewayWafBlocked": "检查代理白名单 / 请求头",
"relayStreamingBug": "升级中转服务;或把 wire 切到 openai-chat;或改用 api.openai.com"
},
"applyFix": "应用此修复",
Expand Down
13 changes: 12 additions & 1 deletion packages/shared/src/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,23 @@ describe('diagnoseGenerateFailure', () => {
expect(result[0]?.cause).toBe('diagnostics.cause.keyInvalid');
});

it('maps 403 to keyInvalid hypothesis', () => {
it('maps plain 403 to keyInvalid hypothesis', () => {
const result = diagnoseGenerateFailure({ ...ctx, status: 403 });
expect(result[0]?.cause).toBe('diagnostics.cause.keyInvalid');
expect(result[0]?.category).toBe('auth');
});

it('maps 403 blocked generation responses to gatewayWafBlocked', () => {
const result = diagnoseGenerateFailure({
...ctx,
status: 403,
message: '403 Your request was blocked.',
});
expect(result[0]?.cause).toBe('diagnostics.cause.gatewayWafBlocked');
expect(result[0]?.category).toBe('gateway-waf-blocked');
expect(result[0]?.suggestedFix?.label).toBe('diagnostics.fix.gatewayWafBlocked');
});

it('maps 422 developer role rejection to unsupported-role with a wire switch fix', () => {
const result = diagnoseGenerateFailure({
...ctx,
Expand Down
26 changes: 26 additions & 0 deletions packages/shared/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type DiagnosticCategory =
| 'reasoning-policy'
| 'model-id-shape'
| 'relay-stream-cutoff'
| 'gateway-waf-blocked'
| 'model-discovery-degraded'
| 'transport-interrupted'
| 'upstream-server-error'
Expand Down Expand Up @@ -312,6 +313,16 @@ function isCustomBaseUrl(baseUrl: string | undefined): boolean {
}
}

function looksLikeGatewayWafBlock(message: string): boolean {
return (
/\byour request was blocked\b/i.test(message) ||
/\brequest blocked\b/i.test(message) ||
/\bwaf\b/i.test(message) ||
/\bcloudflare\b/i.test(message) ||
/\bray id\b/i.test(message)
);
}

export function diagnoseGenerateFailure(ctx: GenerateFailureContext): DiagnosticHypothesis[] {
const message = (ctx.message ?? '').toLowerCase();
const status = ctx.status;
Expand Down Expand Up @@ -461,6 +472,21 @@ export function diagnoseGenerateFailure(ctx: GenerateFailureContext): Diagnostic
];
}

if (status === 403 && looksLikeGatewayWafBlock(message)) {
return [
h({
cause: 'diagnostics.cause.gatewayWafBlocked',
category: 'gateway-waf-blocked',
severity: 'warning',
suggestedFix: {
kind: 'openSettings',
label: 'diagnostics.fix.gatewayWafBlocked',
settingsTab: 'models',
},
}),
];
}

if (status === 400 && message.includes('instructions')) {
return [
h({
Expand Down
Loading