Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/windows-nsis-upgrade-regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ jobs:
${{ runner.temp }}/nsis-packaged-proof/first-interaction/first-send.log
${{ runner.temp }}/security-first-send-trace.zip
${{ runner.temp }}/security-first-send/logs/desktop.log*
${{ runner.temp }}/security-first-send/logs/gateway.log*
${{ runner.temp }}/security-first-send/first-send-main-console.jsonl
candidate/audit-candidate.json
if-no-files-found: warn
Expand Down
19 changes: 19 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,25 @@ OpenSquilla also carries provider registry entries for additional
OpenAI-compatible or self-hosted backends. Use `opensquilla providers list` on
your install to see the current catalog.

Outgoing model requests have a separate JSON body limit of 24 MiB
(25,165,824 bytes) by default. Set
`OPENSQUILLA_PROVIDER_REQUEST_MAX_BODY_BYTES` in the Gateway environment to
change the limit, or set it to `0` to disable this guard, then restart the
Gateway. This counts the UTF-8 bytes actually sent to the provider, including
base64 image data. The model's token budget measures a different limit: an
image can fit its estimated token allowance while making the HTTP body too
large. When the byte limit is exceeded, OpenSquilla attempts safe request-text
compaction and otherwise reports a budget error before sending the request.
An upstream service may enforce a lower body limit.

Tool-produced images are prepared before model delivery, with each image
limited to 2048 pixels on its longest edge and 1.5 MiB of base64 data. If an
tool-image batch is larger than four images or 6 MiB, the model request
includes at most four recent usable images and explicitly states how many were
omitted. This applies to the first request after a tool returns images and to
later history replay. The request-only reduction leaves the stored session
unchanged; omitted images must be loaded again when their details are needed.

Read: [`providers-and-models.md`](providers-and-models.md)

## Video Generation
Expand Down
2 changes: 2 additions & 0 deletions opensquilla-webui/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -4077,6 +4077,8 @@
"credits": "Das Guthaben des Modelldienstkontos reicht nicht aus.",
"modelUnavailable": "Das gewählte Modell ist nicht verfügbar.",
"contextLimit": "Der Inhalt überschreitet die Modellkapazität. Kürzen Sie die Eingabe oder wählen Sie ein Modell mit größerem Kontextfenster.",
"requestTooLarge": "Der Modelldienst hat diese Anfrage wegen ihrer Größe abgelehnt. Entfernen Sie große Anhänge oder beginnen Sie einen neuen Chat mit einer kurzen Zusammenfassung.",
"outboundRequestTooLarge": "OpenSquilla hat die Anfrage gestoppt, weil der zu sendende Inhalt die Größenbegrenzung überschreitet. Entfernen Sie große Anhänge oder beginnen Sie einen neuen Chat mit einer kurzen Zusammenfassung.",
"unsupported": "Dieses Modell unterstützt die Anfrage nicht.",
"imageUnsupported": "Dieses Modell unterstützt keine Bilder.",
"ensembleImageUnsupported": "Dieser Modus unterstützt keine Bilder.",
Expand Down
2 changes: 2 additions & 0 deletions opensquilla-webui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -4180,6 +4180,8 @@
"credits": "The model service account has insufficient credits.",
"modelUnavailable": "The selected model is unavailable.",
"contextLimit": "The content exceeds this model's capacity. Shorten the input or choose a larger-context model.",
"requestTooLarge": "The model service rejected this request as too large. Reduce large attachments or start a new chat with a short summary before trying again.",
"outboundRequestTooLarge": "OpenSquilla stopped this request because the content to send exceeds its size limit. Reduce large attachments or start a new chat with a short summary before trying again.",
"unsupported": "This model does not support this request.",
"imageUnsupported": "This model does not support images.",
"ensembleImageUnsupported": "This mode does not support images.",
Expand Down
2 changes: 2 additions & 0 deletions opensquilla-webui/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -4077,6 +4077,8 @@
"credits": "La cuenta del servicio del modelo no tiene saldo suficiente.",
"modelUnavailable": "El modelo seleccionado no está disponible.",
"contextLimit": "El contenido supera la capacidad del modelo. Acorta la entrada o elige un modelo con más contexto.",
"requestTooLarge": "El servicio del modelo rechazó esta solicitud por su tamaño. Reduce los archivos adjuntos grandes o inicia un chat nuevo con un resumen breve antes de volver a intentarlo.",
"outboundRequestTooLarge": "OpenSquilla detuvo esta solicitud porque el contenido que se iba a enviar supera su límite de tamaño. Reduce los archivos adjuntos grandes o inicia un chat nuevo con un resumen breve.",
"unsupported": "Este modelo no admite esta solicitud.",
"imageUnsupported": "Este modelo no admite imágenes.",
"ensembleImageUnsupported": "Este modo no admite imágenes.",
Expand Down
2 changes: 2 additions & 0 deletions opensquilla-webui/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -4077,6 +4077,8 @@
"credits": "Le solde du compte du service de modèle est insuffisant.",
"modelUnavailable": "Le modèle sélectionné est indisponible.",
"contextLimit": "Le contenu dépasse la capacité du modèle. Raccourcissez l’entrée ou choisissez un modèle avec un contexte plus grand.",
"requestTooLarge": "Le service du modèle a rejeté cette requête car elle est trop volumineuse. Réduisez les pièces jointes volumineuses ou démarrez une nouvelle conversation avec un bref résumé avant de réessayer.",
"outboundRequestTooLarge": "OpenSquilla a arrêté cette requête car le contenu à envoyer dépasse sa limite de taille. Réduisez les pièces jointes volumineuses ou démarrez une nouvelle conversation avec un bref résumé.",
"unsupported": "Ce modèle ne prend pas en charge cette demande.",
"imageUnsupported": "Ce modèle ne prend pas en charge les images.",
"ensembleImageUnsupported": "Ce mode ne prend pas en charge les images.",
Expand Down
2 changes: 2 additions & 0 deletions opensquilla-webui/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -4077,6 +4077,8 @@
"credits": "モデルサービスのアカウント残高が不足しています。",
"modelUnavailable": "選択したモデルは利用できません。",
"contextLimit": "内容がモデルの容量を超えています。入力を短くするか、コンテキストの大きいモデルを選んでください。",
"requestTooLarge": "モデルサービスがリクエストのサイズを理由に拒否しました。大きな添付ファイルを減らすか、新しいチャットで短い要約を伝えてから再試行してください。",
"outboundRequestTooLarge": "送信する内容がサイズ制限を超えるため、OpenSquilla がリクエストを停止しました。大きな添付ファイルを減らすか、新しいチャットで短い要約を伝えてください。",
"unsupported": "このモデルはこのリクエストに対応していません。",
"imageUnsupported": "このモデルは画像に対応していません。",
"ensembleImageUnsupported": "このモードは画像に対応していません。",
Expand Down
2 changes: 2 additions & 0 deletions opensquilla-webui/src/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -4179,6 +4179,8 @@
"credits": "模型服务账户余额不足",
"modelUnavailable": "当前模型不可用",
"contextLimit": "内容超出当前模型容量,请缩短输入或选择更大上下文的模型",
"requestTooLarge": "模型服务拒绝了过大的请求。请减少大附件,或在新对话中提供简短摘要后再试。",
"outboundRequestTooLarge": "OpenSquilla 已拦截超出发送大小限制的请求。请减少大附件,或在新对话中提供简短摘要后再试。",
"unsupported": "当前模型不支持此请求",
"imageUnsupported": "当前模型不支持图片",
"ensembleImageUnsupported": "当前模式不支持图片",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ describe('Chat user error presentation', () => {
['provider_overloaded', 'busy'],
['auth_invalid', 'credentials', 'open-provider-settings'],
['context_overflow', 'contextLimit', 'choose-model'],
['payload_too_large', 'requestTooLarge'],
['wire_payload_too_large', 'outboundRequestTooLarge'],
['unsupported_feature', 'unsupported', 'choose-model'],
['insufficient_credits', 'credits', 'open-provider-settings'],
['model_not_found', 'modelUnavailable', 'choose-model'],
Expand All @@ -35,6 +37,8 @@ describe('Chat user error presentation', () => {
it.each([
['no_provider', 'noProvider', 'open-model-settings'],
['provider_request_too_large', 'contextLimit', 'choose-model'],
['provider_payload_too_large', 'requestTooLarge', undefined],
['provider_wire_payload_too_large', 'outboundRequestTooLarge', undefined],
['provider_request_budget_exhausted', 'contextLimit', 'choose-model'],
['current_turn_context_exhausted', 'contextLimit', 'choose-model'],
['attachment_capacity_too_large', 'contextLimit', 'choose-model'],
Expand Down Expand Up @@ -129,7 +133,7 @@ describe('Chat user error presentation', () => {
expect(chatErrorPresentation({ terminalStatus: 'timeout', code: 'provider_retry_after_deadline' })).toEqual({ messageKey: 'chat.errorMessage.timeout' })
expect(chatErrorPresentation({ failureKind: 'insufficient_credits' })).toEqual({ messageKey: 'chat.errorMessage.credits', action: 'open-provider-settings' })
expect(chatErrorPresentation({ failureKind: 'usage_limit' })).toEqual({ messageKey: 'chat.errorMessage.usageLimit' })
for (const code of ['400', '401', '403', '404', '429', '503']) expect(chatErrorPresentation({ code })).toEqual({ messageKey: 'chat.errorMessage.unknown' })
for (const code of ['400', '401', '403', '404', '413', '429', '503']) expect(chatErrorPresentation({ code })).toEqual({ messageKey: 'chat.errorMessage.unknown' })
})

it('never uses a general retry hint to offer whole-turn replay', () => {
Expand Down
4 changes: 4 additions & 0 deletions opensquilla-webui/src/utils/chat/chatErrorPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const PROVIDER_PRESENTATIONS = new Map<string, ChatErrorPresentation>([
['provider_overloaded', presentation('busy')],
['auth_invalid', presentation('credentials', 'open-provider-settings')],
['context_overflow', presentation('contextLimit', 'choose-model')],
['payload_too_large', presentation('requestTooLarge')],
['wire_payload_too_large', presentation('outboundRequestTooLarge')],
['unsupported_feature', presentation('unsupported', 'choose-model')],
['insufficient_credits', presentation('credits', 'open-provider-settings')],
['usage_limit', presentation('usageLimit')],
Expand Down Expand Up @@ -117,6 +119,8 @@ export function chatErrorPresentation(input: ChatErrorPresentationInput): ChatEr
}
if (causes.includes('ensemble_multimodal_unsupported')) return presentation('ensembleImageUnsupported', 'choose-model')
if (causes.includes('image_input_unsupported')) return presentation('imageUnsupported', 'choose-model')
if (causes.includes('provider_wire_payload_too_large')) return presentation('outboundRequestTooLarge')
if (causes.includes('provider_payload_too_large')) return presentation('requestTooLarge')
if (causes.some(value => CONTEXT_LIMITS.has(value))) return presentation('contextLimit', 'choose-model')
if (causes.some(value => ['attachment_capacity_unknown', 'attachment_capacity_unavailable'].includes(value))) {
return presentation('modelUnavailable', 'open-model-settings')
Expand Down
17 changes: 16 additions & 1 deletion opensquilla-webui/src/utils/chat/providerFailure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ describe('provider terminal metadata', () => {
expect(normalizeTurnOutcome({ ...base, [key]: fields })).toEqual(normalizeTurnOutcome({ ...base, ...fields }))
})

it('keeps remote and local request-size causes distinct in persisted outcomes', () => {
const remote = normalizeTurnOutcome({ turn_id: 'remote', failure_kind: 'payload_too_large' })
const local = normalizeTurnOutcome({ turn_id: 'local', failure_kind: 'wire_payload_too_large' })
expect(remote?.failureKind).toBe('payload_too_large')
expect(local?.failureKind).toBe('wire_payload_too_large')
expect(chatErrorPresentation({ failureKind: remote?.failureKind })).toEqual({
messageKey: 'chat.errorMessage.requestTooLarge',
})
expect(chatErrorPresentation({ failureKind: local?.failureKind })).toEqual({
messageKey: 'chat.errorMessage.outboundRequestTooLarge',
})
})

it.each(['ABCDEF01', 'abcdef0', 'abcdef012', 'abcdef01 ', 123, null, ''])('rejects invalid id %s', errorId => {
const outcome = normalizeTurnOutcome({ turn_id: 't', error_id: errorId })
expect(outcome?.errorId).toBeNull()
Expand All @@ -38,7 +51,7 @@ describe('provider terminal metadata', () => {
it.each(['en', 'zh-Hans', 'de', 'es', 'fr', 'ja'] as const)('localizes allowlisted causes in %s', async locale => {
await loadLocaleMessages(locale)
i18n.global.locale.value = locale
for (const kind of ['rate_limited', 'provider_overloaded', 'auth_invalid', 'context_overflow',
for (const kind of ['rate_limited', 'provider_overloaded', 'auth_invalid', 'context_overflow', 'payload_too_large', 'wire_payload_too_large',
'unsupported_feature', 'insufficient_credits', 'model_not_found', 'transport_transient',
'policy_refusal', 'empty_response', 'malformed_response', 'bad_request'] as const) {
const message = localizedChatErrorMessage('429', 'safe fallback', false, kind)
Expand Down Expand Up @@ -97,6 +110,8 @@ describe('provider terminal metadata', () => {
for (const [code, key] of [
['timeout', 'timeout'], ['llm_timeout', 'timeout'],
['provider_output_truncated', 'responseIncomplete'], ['provider_request_too_large', 'contextLimit'],
['provider_payload_too_large', 'requestTooLarge'],
['provider_wire_payload_too_large', 'outboundRequestTooLarge'],
]) {
expect(localizedChatErrorMessage(code, 'specific guidance', false, 'rate_limited'))
.toBe(i18n.global.t(`chat.errorMessage.${key}`))
Expand Down
4 changes: 3 additions & 1 deletion opensquilla-webui/src/utils/chat/providerFailure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Stable public taxonomy only; never infer a provider cause from prose or HTTP codes. */
const FAILURE_KINDS = new Set([
'rate_limited', 'provider_overloaded', 'auth_invalid', 'context_overflow',
'rate_limited', 'provider_overloaded', 'auth_invalid', 'context_overflow', 'payload_too_large',
'wire_payload_too_large',
'unsupported_feature', 'insufficient_credits', 'usage_limit', 'usage_limit_reached', 'model_not_found', 'transport_transient',
'policy_refusal', 'empty_response', 'malformed_response', 'bad_request',
])
Expand All @@ -17,6 +18,7 @@ export function diagnosticErrorId(value: unknown): string | undefined {
const SPECIAL_TERMINALS = new Set([
'timeout', 'llm_timeout', 'iteration_timeout', 'stream_idle_timeout', 'provider_output_truncated',
'provider_request_too_large', 'provider_request_budget_exhausted', 'current_turn_context_exhausted',
'provider_payload_too_large', 'provider_wire_payload_too_large',
'model_repetition_loop_detected', 'tool_run_budget_exhausted', 'llm_budget_exhausted',
'turn_llm_call_budget_exceeded', 'turn_input_token_budget_exceeded',
'turn_output_token_budget_exceeded', 'turn_billed_cost_budget_exceeded', 'max_iterations',
Expand Down
Loading
Loading