fix(free-tier): the desktop renders a free-tier refusal as its own card, not an OAuth re-login
A welcome-tier 403 classifies as auth_permanent, so the desktop's error surface mapped it to "Your Nous Portal sign-in expired" with a Nous Portal re-login button — the chat sentence never reached the user. Terminal results on the free route now carry a structured free_tier block (kind + the chat sentence); agent/error_surface.py turns it into a free_tier_<kind> code on the provider layer with the sentence as `message`. The desktop gives those codes their own titles, shows the backend sentence as the body, and offers "Sign in with a Nous account" (the free-tier dialog) instead of the OAuth re-login, with Retry only where a later send can succeed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,9 @@ _REASON_TO_LAYER = {
|
||||
# Failures between us and the base_url (not a provider verdict); on a
|
||||
# custom/local endpoint they point at the user's endpoint config.
|
||||
_TRANSPORT_REASONS = {"timeout", "ssl_cert_verification"}
|
||||
# Free-tier kinds where a later send can succeed on its own (a wait, an outage clearing); the
|
||||
# rest need a sign-in or another provider.
|
||||
_FREE_TIER_RETRYABLE_KINDS = {"rate_limited", "at_capacity", "outage"}
|
||||
|
||||
# Deterministic for the request — a bare "Retry" repeats the failure. Fallback
|
||||
# only: current backends stamp the classifier's verdict in ``failure_retryable``.
|
||||
@@ -153,6 +156,15 @@ def build_error_surface_from_result(result: Any, provider: str = "", model: str
|
||||
return _surface(LAYER_DISK, "disk_full", False, provider, model)
|
||||
if result.get("billing_block") or reason in ("billing", "billing_unverified"):
|
||||
return _surface(LAYER_BILLING, reason or "billing", False, provider, model)
|
||||
# The Nous free tier refused or could not serve the turn (``agent/turn_recovery.py``
|
||||
# stamps ``free_tier``): its own code, so a client offers the free sign-in rather than an
|
||||
# OAuth re-login, and the chat sentence rides along as the card body.
|
||||
if isinstance(free_tier := result.get("free_tier"), dict) and free_tier.get("kind"):
|
||||
kind = str(free_tier["kind"])
|
||||
surface = _surface(LAYER_PROVIDER, f"free_tier_{kind}", kind in _FREE_TIER_RETRYABLE_KINDS, provider, model)
|
||||
if message := str(free_tier.get("message") or ""):
|
||||
surface["message"] = message
|
||||
return surface
|
||||
if not reason: # failed result without a classified reason (legacy paths)
|
||||
drop = _looks_like_stream_drop(error_text)
|
||||
return _surface(LAYER_STREAMING if drop else LAYER_PROVIDER, "stream_drop" if drop else "unknown", True, provider, model)
|
||||
|
||||
@@ -263,6 +263,8 @@ def nous_rate_limit_guard(
|
||||
"completed": False,
|
||||
"failed": True,
|
||||
"error": _nous_msg,
|
||||
# The free tier's card body and its sign-in door (agent/error_surface.py).
|
||||
**({"free_tier": {"kind": "rate_limited", "message": _nous_msg}} if _welcome else {}),
|
||||
}, FailoverReason.rate_limit.value, True))
|
||||
except Exception:
|
||||
pass # Never let rate guard break the agent loop
|
||||
|
||||
+29
-2
@@ -719,6 +719,27 @@ def _welcome_tier_guidance(classified: Any, *, model: Any, in_chat: bool) -> str
|
||||
return welcome_route_refusal_copy(str(route), in_chat=in_chat)
|
||||
|
||||
|
||||
def _welcome_surface_kind(classified: Any) -> str:
|
||||
"""The free-tier failure kind a client renders its card from (``error_surface`` code
|
||||
``free_tier_<kind>``): the welcome refusal's reason, or the route refusal; "" otherwise."""
|
||||
ctx = getattr(classified, "error_context", None) or {}
|
||||
refusal = ctx.get("welcome_refusal") if isinstance(ctx, dict) else None
|
||||
if isinstance(refusal, dict):
|
||||
reason = str(refusal.get("reason") or "")
|
||||
return {"admission_closed": "at_capacity", "feature_not_free": "model_not_free"}.get(reason, reason) or "refused"
|
||||
route = ctx.get("welcome_route") if isinstance(ctx, dict) else None
|
||||
if route == "tier_disabled":
|
||||
return "disabled"
|
||||
return "route" if route else ""
|
||||
|
||||
|
||||
def _stamp_free_tier(result: Dict[str, Any], kind: str, message: str) -> Dict[str, Any]:
|
||||
"""Structured free-tier failure block: ``error_surface`` keys its code on ``kind`` and a client
|
||||
shows ``message`` (the chat sentence) as the card body instead of its own generic copy."""
|
||||
result["free_tier"] = {"kind": kind or "refused", "message": message}
|
||||
return result
|
||||
|
||||
|
||||
def _welcome_outage_copy(base_url: Any, classified: Any) -> str:
|
||||
"""On the Nous free tier, a transport / server failure that outlived every retry reads as one
|
||||
plain sentence (the free model is having trouble) rather than the technical summary. Empty
|
||||
@@ -859,6 +880,8 @@ def nonretryable_client_error_result(
|
||||
"failure_reason": classified.reason.value,
|
||||
"failure_retryable": bool(classified.retryable),
|
||||
})
|
||||
if _welcome_hint:
|
||||
_stamp_free_tier(result, _welcome_surface_kind(classified), _final_response)
|
||||
return result
|
||||
|
||||
|
||||
@@ -943,6 +966,7 @@ def max_retries_exhausted_result(
|
||||
agent._persist_session(messages, conversation_history)
|
||||
_billing_block = None
|
||||
_billing_unverified = False
|
||||
_free_tier_kind = ""
|
||||
if _is_billing:
|
||||
_billing_unverified = classified.billing_unverified
|
||||
_final_response = _billing_terminal_label(_final_summary, _billing_unverified)
|
||||
@@ -961,8 +985,9 @@ def max_retries_exhausted_result(
|
||||
)
|
||||
if _welcome_hint:
|
||||
_final_response = _welcome_tier_guidance(classified, model=model, in_chat=True)
|
||||
else:
|
||||
_final_response = _welcome_outage_copy(base_url, classified) or _final_response
|
||||
_free_tier_kind = _welcome_surface_kind(classified)
|
||||
elif _outage := _welcome_outage_copy(base_url, classified):
|
||||
_final_response, _free_tier_kind = _outage, "outage"
|
||||
if _is_thinking_timeout:
|
||||
# Thinking-timeout guidance overrides stream-drop guidance, which would wrongly
|
||||
# suggest splitting large file writes.
|
||||
@@ -985,6 +1010,8 @@ def max_retries_exhausted_result(
|
||||
# Present only for billing walls: (provider, billing_url, is_nous, message).
|
||||
"billing_block": _billing_block,
|
||||
})
|
||||
if _free_tier_kind:
|
||||
_stamp_free_tier(result, _free_tier_kind, _final_response)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import { markAssistantIdSpoken } from '@/lib/spoken-reply'
|
||||
import { useEnterAnimation } from '@/lib/use-enter-animation'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { playSpeechText, stopVoicePlayback } from '@/lib/voice-playback'
|
||||
import { openFreeTierSignIn } from '@/store/free-tier-sign-in'
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import { startManualProviderOAuth } from '@/store/onboarding'
|
||||
import { $activeGatewayProfile, normalizeProfileKey, requestFreshSession } from '@/store/profile'
|
||||
@@ -639,6 +640,13 @@ const ErrorRecoveryActions: FC = () => {
|
||||
startManualProviderOAuth(surface.provider, key === 'default' ? undefined : key)
|
||||
}, [gatewayProfile, surface])
|
||||
|
||||
// The free tier's door: the same dialog the status-bar chip and the first-launch
|
||||
// intro open. Signing in is free and lifts every free-tier refusal.
|
||||
const signInFreeTier = useCallback(() => {
|
||||
triggerHaptic('submit')
|
||||
openFreeTierSignIn()
|
||||
}, [])
|
||||
|
||||
// Reveal a local folder through Electron; `logsRoot` is the profile's
|
||||
// HERMES_HOME/logs, and its parent is the Hermes data folder itself (what
|
||||
// the user needs to see to free space after a disk-full failure).
|
||||
@@ -714,6 +722,12 @@ const ErrorRecoveryActions: FC = () => {
|
||||
{copy.errorSignInAgain(surface.providerLabel || surface.provider)}
|
||||
</button>
|
||||
)}
|
||||
{plan.signInFreeTier && (
|
||||
<button className="aui-error-action" onClick={signInFreeTier} type="button">
|
||||
<KeyRound className="size-3" />
|
||||
{copy.errorSignInFreeTier}
|
||||
</button>
|
||||
)}
|
||||
{plan.updateApiKey && inRouter && (
|
||||
<SettingsLinkAction
|
||||
icon={<KeyRound className="size-3" />}
|
||||
|
||||
@@ -4052,6 +4052,36 @@ export const en: Translations = {
|
||||
disk_full: {
|
||||
title: 'Disk full',
|
||||
body: 'Your disk is full, so Hermes could not save this conversation. Free some space, then retry.'
|
||||
},
|
||||
// Nous free tier. The body is normally the backend's own sentence (it names the wait
|
||||
// and the way forward); these bodies stand in for an older backend that sent none.
|
||||
free_tier_disabled: {
|
||||
title: 'Using Hermes without signing in is switched off right now',
|
||||
body: "Sign in with a Nous account to keep chatting, it's free and keeps the free model."
|
||||
},
|
||||
free_tier_rate_limited: {
|
||||
title: "You've used up the allowance for chatting without signing in",
|
||||
body: "It refreshes shortly. Sign in with a Nous account for a bigger allowance, it's free."
|
||||
},
|
||||
free_tier_at_capacity: {
|
||||
title: 'Chatting without signing in is really busy right now',
|
||||
body: "Sign in to skip the queue, it's free, or try again in a little while."
|
||||
},
|
||||
free_tier_model_not_free: {
|
||||
title: "That model isn't available without signing in",
|
||||
body: "Hermes uses the free model for now. Sign in with a Nous account for more models, it's free."
|
||||
},
|
||||
free_tier_route: {
|
||||
title: "Hermes couldn't reach the free model on this route",
|
||||
body: "Sign in with a Nous account, it's free, or check the NOUS_INFERENCE_BASE_URL setting."
|
||||
},
|
||||
free_tier_outage: {
|
||||
title: 'The free model is having trouble responding right now',
|
||||
body: 'Try sending your message again in a minute.'
|
||||
},
|
||||
free_tier_refused: {
|
||||
title: "Hermes couldn't send that without signing in",
|
||||
body: 'Signing in with a Nous account is free.'
|
||||
}
|
||||
},
|
||||
errorAuthKinds: {
|
||||
@@ -4076,6 +4106,7 @@ export const en: Translations = {
|
||||
errorOpenHermesFolderFailed: 'Could not open the Hermes folder',
|
||||
errorUpdateApiKey: 'Update API key',
|
||||
errorSignInAgain: provider => `Sign in to ${provider} again`,
|
||||
errorSignInFreeTier: 'Sign in with a Nous account',
|
||||
errorOauthExpired: provider =>
|
||||
`Your ${provider} sign-in has expired or was revoked. Sign in again to keep chatting.`,
|
||||
errorOpenLogs: 'Open logs',
|
||||
|
||||
@@ -3511,6 +3511,8 @@ export interface Translations {
|
||||
/** One-click recovery for an expired/revoked OAuth grant: re-runs that
|
||||
* provider's sign-in flow (auth layer, authKind 'oauth'). */
|
||||
errorSignInAgain: (provider: string) => string
|
||||
/** Free-tier refusals: opens the free sign-in dialog (signing in is free and lifts the refusal). */
|
||||
errorSignInFreeTier: string
|
||||
/** Explains WHY the turn failed for an OAuth 401 — the raw body
|
||||
* ("HTTP 401: User not found.") doesn't say "sign in again". */
|
||||
errorOauthExpired: (provider: string) => string
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import type { ErrorCardCopy, Translations } from '@/i18n/types'
|
||||
|
||||
import { errorCardKey, type ErrorSurface } from './error-surface'
|
||||
import { errorCardKey, type ErrorSurface, isFreeTierSurface } from './error-surface'
|
||||
|
||||
export interface ErrorCardText {
|
||||
title: string
|
||||
@@ -45,7 +45,12 @@ export function errorCardText(
|
||||
if ('code' in key) {
|
||||
const copy = thread.errorCodes[key.code]
|
||||
|
||||
return { body: render(copy.body, provider), title: render(copy.title, provider) }
|
||||
// A free-tier refusal arrives with the backend's own sentence (the wait, the
|
||||
// model, the way forward); the table body is only the fallback for an older backend.
|
||||
return {
|
||||
body: (isFreeTierSurface(surface) && surface?.message) || render(copy.body, provider),
|
||||
title: render(copy.title, provider)
|
||||
}
|
||||
}
|
||||
|
||||
return { body: thread.errorLayerBodies[key.layer], title: thread.errorLayers[key.layer] }
|
||||
|
||||
@@ -138,3 +138,43 @@ describe('error copy never names a hidden Retry', () => {
|
||||
expect(errorRecoveryPlan(surface).retry).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('free-tier refusals', () => {
|
||||
const surface = parseErrorSurface({
|
||||
code: 'free_tier_disabled',
|
||||
layer: 'provider',
|
||||
message: ' Using Hermes without signing in is switched off right now. To sign in: /login. ',
|
||||
provider: 'nous',
|
||||
retryable: false
|
||||
})!
|
||||
|
||||
it('carries the backend sentence and offers the free sign-in, never an OAuth re-login', () => {
|
||||
expect(surface.message).toBe('Using Hermes without signing in is switched off right now. To sign in: /login.')
|
||||
const plan = errorRecoveryPlan(surface)
|
||||
expect(plan.signInFreeTier).toBe(true)
|
||||
expect(plan.signInAgain).toBe(false)
|
||||
expect(plan.retry).toBe(false)
|
||||
expect(plan.switchProvider).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the backend sentence as the body under its own title', () => {
|
||||
const { body, title } = errorCardText(en.assistant.thread, surface)
|
||||
expect(title).toBe(en.assistant.thread.errorCodes.free_tier_disabled.title)
|
||||
expect(body).toBe(surface.message)
|
||||
})
|
||||
|
||||
it('falls back to the table body when an older backend sent no sentence', () => {
|
||||
const bare = parseErrorSurface({ code: 'free_tier_rate_limited', layer: 'provider', retryable: true })!
|
||||
expect(errorCardText(en.assistant.thread, bare).body).toBe(en.assistant.thread.errorCodes.free_tier_rate_limited.body)
|
||||
expect(errorRecoveryPlan(bare).retry).toBe(true)
|
||||
})
|
||||
|
||||
it('every free-tier code has copy and the copy never blames the free model', () => {
|
||||
for (const code of ERROR_CODE_KEYS.filter(key => key.startsWith('free_tier_'))) {
|
||||
const copy = en.assistant.thread.errorCodes[code]
|
||||
const text = `${typeof copy.title === 'string' ? copy.title : ''} ${typeof copy.body === 'string' ? copy.body : ''}`.toLowerCase()
|
||||
expect(text).not.toMatch(/free (service|model|tier) is (off|switched off|unavailable|down)/)
|
||||
expect(text).not.toMatch(/anonymous|guest|credential|token|rate limit/)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +46,16 @@ export const ERROR_CODE_KEYS = [
|
||||
'empty_response',
|
||||
'loop_error',
|
||||
'SESSION_NOT_OWNED',
|
||||
'disk_full'
|
||||
'disk_full',
|
||||
// The Nous free tier refused or could not serve the turn (agent/error_surface.py
|
||||
// `free_tier_<kind>`). The backend's sentence rides in `message` and is the card body.
|
||||
'free_tier_disabled',
|
||||
'free_tier_rate_limited',
|
||||
'free_tier_at_capacity',
|
||||
'free_tier_model_not_free',
|
||||
'free_tier_route',
|
||||
'free_tier_outage',
|
||||
'free_tier_refused'
|
||||
] as const
|
||||
|
||||
export type ErrorCodeKey = (typeof ERROR_CODE_KEYS)[number]
|
||||
@@ -72,6 +81,9 @@ export interface ErrorSurface {
|
||||
* (OPENAI_API_KEY). Deep-links Settings → Keys to that row. Absent from
|
||||
* older backends. */
|
||||
apiKeyEnv?: string
|
||||
/** Free-tier codes: the backend's own plain sentence for this failure (it
|
||||
* names the wait, the model, the way forward). Shown as the card body. */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/** Validate a wire payload into an ErrorSurface, or null when absent/garbled. */
|
||||
@@ -85,6 +97,7 @@ export function parseErrorSurface(value: unknown): ErrorSurface | null {
|
||||
auth_kind?: unknown
|
||||
code?: unknown
|
||||
layer?: unknown
|
||||
message?: unknown
|
||||
model?: unknown
|
||||
provider?: unknown
|
||||
provider_label?: unknown
|
||||
@@ -105,10 +118,17 @@ export function parseErrorSurface(value: unknown): ErrorSurface | null {
|
||||
...(typeof raw.model === 'string' && raw.model ? { model: raw.model } : {}),
|
||||
...(raw.auth_kind === 'oauth' || raw.auth_kind === 'api_key' ? { authKind: raw.auth_kind } : {}),
|
||||
...(typeof raw.provider_label === 'string' && raw.provider_label ? { providerLabel: raw.provider_label } : {}),
|
||||
...(typeof raw.api_key_env === 'string' && raw.api_key_env ? { apiKeyEnv: raw.api_key_env } : {})
|
||||
...(typeof raw.api_key_env === 'string' && raw.api_key_env ? { apiKeyEnv: raw.api_key_env } : {}),
|
||||
...(typeof raw.message === 'string' && raw.message.trim() ? { message: raw.message.trim() } : {})
|
||||
}
|
||||
}
|
||||
|
||||
/** True when the Nous free tier refused or could not serve the turn: the way
|
||||
* forward is the free sign-in (or another provider), never an OAuth re-login. */
|
||||
export function isFreeTierSurface(surface: ErrorSurface | null | undefined): boolean {
|
||||
return typeof surface?.code === 'string' && surface.code.startsWith('free_tier_')
|
||||
}
|
||||
|
||||
/** True when the failed turn's provider rejected an OAuth grant — the
|
||||
* one-click recovery is re-running that provider's sign-in, not editing keys. */
|
||||
export function isOAuthReauthSurface(surface: ErrorSurface | null | undefined): surface is ErrorSurface & {
|
||||
@@ -161,6 +181,8 @@ export interface ErrorRecoveryPlan {
|
||||
updateApiKey: boolean
|
||||
/** Re-run the provider's OAuth sign-in (auth, oauth). */
|
||||
signInAgain: boolean
|
||||
/** Open the free-tier sign-in dialog (free_tier_* codes): signing in is free and lifts the refusal. */
|
||||
signInFreeTier: boolean
|
||||
/** Settings → Models deep link. */
|
||||
switchProvider: boolean
|
||||
}
|
||||
@@ -197,6 +219,7 @@ export function errorRecoveryPlan(surface: ErrorSurface | null | undefined): Err
|
||||
// natural second click.
|
||||
retry: !surface || surface.retryable || oauthReauth || apiKeyRejected,
|
||||
signInAgain: oauthReauth,
|
||||
signInFreeTier: isFreeTierSurface(surface),
|
||||
startNewSession: false,
|
||||
switchProvider: surface != null && SWITCH_PROVIDER_LAYERS.includes(surface.layer),
|
||||
updateApiKey: apiKeyRejected
|
||||
|
||||
@@ -229,3 +229,32 @@ def test_exception_never_raises_on_weird_input():
|
||||
|
||||
# Must not raise, whatever it returns.
|
||||
build_error_surface_from_exception(Hostile("x"))
|
||||
|
||||
|
||||
# ── Nous free tier ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_free_tier_block_gets_its_own_code_and_carries_the_sentence():
|
||||
"""A free-tier refusal is never an OAuth re-login: its own ``free_tier_<kind>`` code on the
|
||||
provider layer, with the chat sentence riding along as the card body."""
|
||||
result = _failed_result("auth_permanent", error="HTTP 403: no permissions",
|
||||
free_tier={"kind": "disabled", "message": "Using Hermes without signing in is switched off."})
|
||||
surface = build_error_surface_from_result(result, provider="nous", model="nous/welcome")
|
||||
assert surface["layer"] == LAYER_PROVIDER and surface["code"] == "free_tier_disabled"
|
||||
assert surface["retryable"] is False and "auth_kind" not in surface
|
||||
assert surface["message"] == "Using Hermes without signing in is switched off."
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind,retryable", [
|
||||
("rate_limited", True), ("at_capacity", True), ("outage", True),
|
||||
("disabled", False), ("model_not_free", False), ("route", False), ("refused", False),
|
||||
])
|
||||
def test_free_tier_kinds_say_whether_a_later_send_can_succeed(kind, retryable):
|
||||
surface = build_error_surface_from_result(_failed_result("rate_limit", free_tier={"kind": kind}), provider="nous")
|
||||
assert surface["code"] == f"free_tier_{kind}" and surface["retryable"] is retryable
|
||||
assert "message" not in surface
|
||||
|
||||
|
||||
def test_a_free_tier_block_without_a_kind_is_ignored():
|
||||
surface = build_error_surface_from_result(_failed_result("auth_permanent", free_tier={}), provider="nous")
|
||||
assert surface["code"] == "auth_permanent" and surface["layer"] == LAYER_AUTH
|
||||
|
||||
@@ -159,3 +159,59 @@ class TestOutageCopy:
|
||||
from agent.turn_recovery import _welcome_outage_copy
|
||||
assert _welcome_outage_copy(PAID, SimpleNamespace(reason=FailoverReason.timeout)) == ""
|
||||
assert _welcome_outage_copy(WELCOME, SimpleNamespace(reason=FailoverReason.rate_limit)) == ""
|
||||
|
||||
|
||||
class TestTerminalResultsCarryTheFreeTierBlock:
|
||||
"""The terminal results stamp ``free_tier`` so a client renders the free tier's own card
|
||||
(agent/error_surface.py) instead of an OAuth re-login."""
|
||||
|
||||
@staticmethod
|
||||
def _terminal_agent():
|
||||
agent = _agent(_dump_api_request_debug=lambda *a, **k: None, _flush_status_buffer=lambda: None,
|
||||
_summarize_api_error=lambda e: "HTTP 403: no permissions", _emit_status=lambda *a: None,
|
||||
_persist_session=lambda *a: None, _plines=lambda *a: None, _buffer_status=lambda *a: None,
|
||||
_rate_limit_state=None, _has_pending_fallback=lambda: False)
|
||||
return agent
|
||||
|
||||
def test_a_dark_tier_403_is_stamped_disabled_with_the_chat_sentence(self):
|
||||
from agent.turn_recovery import nonretryable_client_error_result
|
||||
err = _generic_403()
|
||||
classified = _classify(err)
|
||||
result = nonretryable_client_error_result(
|
||||
self._terminal_agent(), err, classified, status_code=403, api_kwargs=None, api_messages=[],
|
||||
messages=[], conversation_history=[], api_call_count=1, approx_tokens=10,
|
||||
provider="nous", base_url=WELCOME, model="nous/welcome")
|
||||
assert result["free_tier"] == {"kind": "disabled", "message": result["final_response"]}
|
||||
assert "switched off" in result["final_response"] and "/login" in result["final_response"]
|
||||
assert result["error"] == "HTTP 403: no permissions" # the technical detail stays in the log line
|
||||
|
||||
def test_an_exhausted_capacity_refusal_is_stamped_at_capacity(self):
|
||||
from agent.turn_recovery import max_retries_exhausted_result
|
||||
err = _refusal("at_capacity", retry_after=30)
|
||||
classified = _classify(err)
|
||||
result = max_retries_exhausted_result(
|
||||
self._terminal_agent(), err, classified, max_retries=3, is_rate_limited=True, error_msg="429",
|
||||
api_kwargs=None, api_messages=[], messages=[], conversation_history=[], api_call_count=3,
|
||||
approx_tokens=10, provider="nous", base_url=WELCOME, model="nous/welcome")
|
||||
assert result["free_tier"]["kind"] == "at_capacity"
|
||||
assert result["free_tier"]["message"] == result["final_response"]
|
||||
assert "really busy" in result["final_response"]
|
||||
|
||||
def test_a_spent_outage_on_the_welcome_host_is_stamped_outage(self):
|
||||
from agent.turn_recovery import max_retries_exhausted_result
|
||||
err = _gateway_error(503, {"status": 503, "message": "The requested model is currently unavailable."})
|
||||
classified = _classify(err)
|
||||
result = max_retries_exhausted_result(
|
||||
self._terminal_agent(), err, classified, max_retries=3, is_rate_limited=False, error_msg="503",
|
||||
api_kwargs=None, api_messages=[], messages=[], conversation_history=[], api_call_count=3,
|
||||
approx_tokens=10, provider="nous", base_url=WELCOME, model="nous/welcome")
|
||||
assert result["free_tier"]["kind"] == "outage"
|
||||
|
||||
def test_the_same_outage_on_the_paid_host_is_not_stamped(self):
|
||||
from agent.turn_recovery import max_retries_exhausted_result
|
||||
err = _gateway_error(503, {"status": 503, "message": "The requested model is currently unavailable."})
|
||||
result = max_retries_exhausted_result(
|
||||
self._terminal_agent(), err, _classify(err, base_url=PAID), max_retries=3, is_rate_limited=False,
|
||||
error_msg="503", api_kwargs=None, api_messages=[], messages=[], conversation_history=[],
|
||||
api_call_count=3, approx_tokens=10, provider="nous", base_url=PAID, model="hermes-4")
|
||||
assert "free_tier" not in result
|
||||
|
||||
Reference in New Issue
Block a user