mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-28 20:44:50 +02:00
[eric] providers: an Anthropic key that spans workspaces sends anthropic-workspace-id on every lane, with a Workspace ID field beside the key and a card that names the fix
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C9zwUaHucUgrdxvK8FvjYT
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f06643fcd6
commit
48886e4746
@@ -704,6 +704,7 @@ async def probe_model(body: dict):
|
||||
import anthropic
|
||||
client = None
|
||||
|
||||
from backend.apps.settings.credentials import own_key_anthropic_client as p_own_key_client
|
||||
# Routing mirrors agent_manager: prefix takes precedence over Pro.
|
||||
resolved_is_9router = (
|
||||
isinstance(resolved, str)
|
||||
@@ -715,7 +716,7 @@ async def probe_model(body: dict):
|
||||
return {"ok": True, "skipped": True}
|
||||
client = anthropic.AsyncAnthropic(api_key="9router", base_url="http://localhost:20128")
|
||||
elif route == "api" and api_type == "anthropic" and getattr(settings, "anthropic_api_key", None):
|
||||
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||
client = p_own_key_client(settings)
|
||||
elif api_type == "anthropic" and connection_mode == "openswarm-pro":
|
||||
bearer = getattr(settings, "openswarm_bearer_token", "") or ""
|
||||
proxy_url = (getattr(settings, "openswarm_proxy_url", None) or "https://api.openswarm.com").rstrip("/")
|
||||
@@ -723,7 +724,7 @@ async def probe_model(body: dict):
|
||||
return {"ok": True, "skipped": True}
|
||||
client = anthropic.AsyncAnthropic(auth_token=bearer, base_url=proxy_url)
|
||||
elif api_type == "anthropic" and getattr(settings, "anthropic_api_key", None):
|
||||
client = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||
client = p_own_key_client(settings)
|
||||
else:
|
||||
if not p_9r_running():
|
||||
return {"ok": True, "skipped": True}
|
||||
|
||||
@@ -308,6 +308,16 @@ def is_translation_error(exc: BaseException, extra_text: str = "") -> bool:
|
||||
return bool(P_TRANSLATION_ERROR_PATTERNS.search(combined))
|
||||
|
||||
|
||||
# Anthropic's own wording for an identity-linked key sent without a workspace id (400), with one it
|
||||
# cannot use (404 "Workspace `wrkspc_...` not found."), or with a malformed one.
|
||||
P_WORKSPACE_ID_RE = re.compile(r"anthropic-workspace-id|workspace\s+`?wrkspc_[\w-]*`?\s+not\s+found", re.IGNORECASE)
|
||||
|
||||
|
||||
def is_workspace_id_error(exc: BaseException, extra_text: str = "") -> bool:
|
||||
"""True when Anthropic refused the request over the workspace id, not the key itself."""
|
||||
return bool(P_WORKSPACE_ID_RE.search(f"{exc!s}\n{extra_text}"))
|
||||
|
||||
|
||||
@typechecked
|
||||
def is_auth_error(exc: BaseException, extra_text: str = "") -> bool:
|
||||
"""True when the upstream error is a 401/403 auth failure.
|
||||
@@ -320,6 +330,10 @@ def is_auth_error(exc: BaseException, extra_text: str = "") -> bool:
|
||||
combined = f"{exc!s}\n{extra_text}".strip()
|
||||
if not combined:
|
||||
return False
|
||||
# A missing or wrong workspace id is a credential problem the user fixes in Settings; it used to land
|
||||
# here only because "invalid_request_error ... API key" happened to match the generic regex below.
|
||||
if is_workspace_id_error(exc, extra_text):
|
||||
return True
|
||||
# A tool-schema translation 400 can carry provider/connection wording that trips the auth regex below; it isn't auth, so don't claim it is.
|
||||
if is_translation_error(exc, extra_text):
|
||||
return False
|
||||
|
||||
@@ -63,8 +63,9 @@ async def configure_provider_env(
|
||||
api_route_provider = (model_entry or {}).get("api") if is_pinned_api_route else None
|
||||
|
||||
if is_pinned_api_route and api_route_provider == "anthropic" and getattr(global_settings, "anthropic_api_key", None):
|
||||
from backend.apps.settings.credentials import own_key_cli_env as p_own_key_cli_env
|
||||
options_kwargs["env"] = {
|
||||
"ANTHROPIC_API_KEY": global_settings.anthropic_api_key,
|
||||
**p_own_key_cli_env(global_settings),
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
# Pin subagents so they don't drift back to the proxy.
|
||||
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-4-6",
|
||||
@@ -177,7 +178,8 @@ async def configure_provider_env(
|
||||
options_kwargs["env"]["CLAUDE_CODE_SUBAGENT_MODEL"] = "claude-haiku-4-5-20251001"
|
||||
logger.info(f"[MCP-DEBUG] Using OpenSwarm cloud proxy at {proxy_url}")
|
||||
elif api_type == "anthropic" and not resolved_is_9router and global_settings.anthropic_api_key:
|
||||
options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key}
|
||||
from backend.apps.settings.credentials import own_key_cli_env as p_own_key_cli_env_fallback
|
||||
options_kwargs["env"] = p_own_key_cli_env_fallback(global_settings)
|
||||
logger.info("[MCP-DEBUG] Using direct Anthropic API key")
|
||||
elif await router_available(global_settings):
|
||||
# Gemini-bound ids go through the local proxy for schema scrubbing; everything else hits 9Router directly.
|
||||
|
||||
@@ -8,7 +8,12 @@ import logging
|
||||
from typing import List
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.manager.streaming.provider_error_speech import CODEX_ROTATION_RESEND_NOTICE, CODEX_ROTATION_RETRY_NOTICE
|
||||
from backend.apps.agents.manager.streaming.provider_error_speech import (
|
||||
ANTHROPIC_WORKSPACE_ID_MISSING_NOTICE,
|
||||
ANTHROPIC_WORKSPACE_ID_WRONG_NOTICE,
|
||||
CODEX_ROTATION_RESEND_NOTICE,
|
||||
CODEX_ROTATION_RETRY_NOTICE,
|
||||
)
|
||||
|
||||
from backend.apps.agents.core.models import AgentSession, Message
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
@@ -25,6 +30,7 @@ from backend.apps.agents.core.error_classify import (
|
||||
is_out_of_tokens,
|
||||
has_auth_status,
|
||||
is_auth_error,
|
||||
is_workspace_id_error,
|
||||
is_content_policy_block,
|
||||
is_cert_failure,
|
||||
is_cli_binary_missing,
|
||||
@@ -537,7 +543,12 @@ async def handle_run_error(e: Exception, session: AgentSession, session_id: str,
|
||||
logger.info(f"auth self-heal armed for {session_id} (codex_rotation={p_codex_rotation})")
|
||||
return
|
||||
# Codex/OpenAI subscription tokens rotate every ~2-3 minutes, the user sees the rotation window as a 401 with "reset after 1m 59s" or similar. Don't ask them to reconnect; just tell them to wait it out and retry.
|
||||
if (
|
||||
# First, above every other reading: the key is fine, the workspace id is missing or wrong.
|
||||
if is_workspace_id_error(e, extra_text=p_stderr_tail):
|
||||
p_has_workspace = bool((getattr(load_settings(), "anthropic_workspace_id", None) or "").strip())
|
||||
friendly_msg = ANTHROPIC_WORKSPACE_ID_WRONG_NOTICE if p_has_workspace else ANTHROPIC_WORKSPACE_ID_MISSING_NOTICE
|
||||
reason = "anthropic_workspace_id"
|
||||
elif (
|
||||
("codex/" in p_combined or "[codex/" in p_combined or p_model.startswith(("cx/", "gpt-")))
|
||||
and ("authentication token is expired" in p_combined or "authentication token has expired" in p_combined or has_auth_status(p_combined))
|
||||
):
|
||||
|
||||
@@ -266,6 +266,18 @@ CODEX_ROTATION_RETRY_NOTICE = (
|
||||
"GPT subscription token just rotated (automatic, every couple minutes). Retrying your request "
|
||||
"automatically in about a minute, no action needed."
|
||||
)
|
||||
# Anthropic's personal and service-account keys can span workspaces; such a key needs the workspace id
|
||||
# on every request, and the generic "re-enter your key" card sent one user around in circles (2026-09-03).
|
||||
ANTHROPIC_WORKSPACE_ID_MISSING_NOTICE = (
|
||||
"Your Anthropic API key is linked to your account and can act in more than one workspace, so "
|
||||
"Anthropic needs to know which one to use. Open Settings > Models and enter the Workspace ID next "
|
||||
"to your Anthropic key. You can find it in the ID column at platform.claude.com/settings/workspaces."
|
||||
)
|
||||
ANTHROPIC_WORKSPACE_ID_WRONG_NOTICE = (
|
||||
"Anthropic did not accept the Workspace ID saved next to your Anthropic key. Open Settings > Models "
|
||||
"and check it against the ID column at platform.claude.com/settings/workspaces; if the key is scoped "
|
||||
"to a single workspace, leave the field empty."
|
||||
)
|
||||
CODEX_ROTATION_RESEND_NOTICE = (
|
||||
"GPT subscription token just rotated (automatic, every couple minutes). Send your message again "
|
||||
"in about a minute and it will go through; nothing to reconnect. If it keeps happening, open "
|
||||
|
||||
@@ -358,9 +358,11 @@ def p_pick_upstream(model: str) -> tuple[str, dict[str, str]]:
|
||||
return (proxy, {"Authorization": f"Bearer {bearer}"})
|
||||
ak = getattr(s, "anthropic_api_key", "") or ""
|
||||
if ak.strip():
|
||||
from backend.apps.settings.credentials import anthropic_workspace_header as p_workspace_header
|
||||
return ("https://api.anthropic.com", {
|
||||
"x-api-key": ak.strip(),
|
||||
"anthropic-version": "2023-06-01",
|
||||
**p_workspace_header(s),
|
||||
})
|
||||
|
||||
return ("http://127.0.0.1:20128", {"x-api-key": "9router"})
|
||||
|
||||
@@ -125,6 +125,34 @@ def get_provider_credentials(settings: AppSettings, provider: str) -> dict[str,
|
||||
raise ValueError(f"No credentials for provider: {provider}")
|
||||
|
||||
|
||||
def anthropic_workspace_header(settings: AppSettings) -> dict[str, str]:
|
||||
"""Anthropic's identity-linked keys (personal and service-account) can span workspaces; such a key
|
||||
must say which workspace every request acts in or the API answers 400 before the model is reached."""
|
||||
workspace = (getattr(settings, "anthropic_workspace_id", None) or "").strip()
|
||||
return {"anthropic-workspace-id": workspace} if workspace else {}
|
||||
|
||||
|
||||
def own_key_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic:
|
||||
"""The user's own Anthropic key as an SDK client. Every aux call on the key lane builds its client
|
||||
here, so the workspace header cannot be present on one door and missing on another."""
|
||||
import anthropic
|
||||
|
||||
return anthropic.AsyncAnthropic(
|
||||
api_key=settings.anthropic_api_key,
|
||||
default_headers=anthropic_workspace_header(settings) or None,
|
||||
)
|
||||
|
||||
|
||||
def own_key_cli_env(settings: AppSettings) -> dict[str, str]:
|
||||
"""The user's own Anthropic key as the CLI's env. ANTHROPIC_CUSTOM_HEADERS is the CLI's documented
|
||||
way to add a request header; a key with no workspace id gets exactly the env it always did."""
|
||||
env = {"ANTHROPIC_API_KEY": settings.anthropic_api_key or ""}
|
||||
header = anthropic_workspace_header(settings)
|
||||
if header:
|
||||
env["ANTHROPIC_CUSTOM_HEADERS"] = "\n".join(f"{name}: {value}" for name, value in header.items())
|
||||
return env
|
||||
|
||||
|
||||
def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic:
|
||||
"""Return an AsyncAnthropic client for the user's current connection mode."""
|
||||
import anthropic
|
||||
@@ -138,7 +166,7 @@ def get_anthropic_client(settings: AppSettings) -> anthropic.AsyncAnthropic:
|
||||
|
||||
# Prefer the user's own API key when present.
|
||||
if settings.anthropic_api_key:
|
||||
return anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)
|
||||
return own_key_anthropic_client(settings)
|
||||
|
||||
# Fall back to 9Router (free for users with Claude/ChatGPT/Gemini subscriptions).
|
||||
if p_check_9router():
|
||||
|
||||
@@ -118,6 +118,8 @@ class AppSettings(BaseModel):
|
||||
# Off = agents can still READ your settings (redacted) but every SettingsWrite is refused.
|
||||
agent_settings_write_enabled: bool = True
|
||||
anthropic_api_key: Optional[str] = None
|
||||
# A personal or service-account key that spans workspaces must name the one each request acts in (wrkspc_...).
|
||||
anthropic_workspace_id: Optional[str] = None
|
||||
browser_homepage: str = "https://www.google.com"
|
||||
# Opt-in: let a blocked browser agent borrow the sign-in you already have in your everyday
|
||||
# browser instead of stopping to ask you to log in again. Default OFF because reading your real
|
||||
|
||||
@@ -225,7 +225,7 @@ async def apply_settings_update(body: AppSettings, protect_fields: set[str] | No
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
secret_keys = {"anthropic_api_key", "openai_api_key", "google_api_key", "openrouter_api_key",
|
||||
secret_keys = {"anthropic_api_key", "anthropic_workspace_id", "openai_api_key", "google_api_key", "openrouter_api_key",
|
||||
"claude_subscription_token", "openai_subscription_token", "gemini_subscription_token",
|
||||
"openswarm_bearer_token", "free_trial_token", "installation_id", "analytics_token"}
|
||||
safe = {k: v for k, v in body.model_dump().items() if k not in secret_keys}
|
||||
@@ -380,6 +380,7 @@ async def reset_system_prompt():
|
||||
# A preferences reset (the iOS "Reset All Settings" analogue): everything back to defaults EXCEPT the things a "reset my preferences" click must never silently sever, your connections (server-owned subscription fields AND your pasted provider credentials) and your identity. Hard-erase is the separate flow.
|
||||
P_RESET_PRESERVE_FIELDS = SERVER_OWNED_FIELDS + (
|
||||
"anthropic_api_key",
|
||||
"anthropic_workspace_id",
|
||||
"openai_api_key",
|
||||
"google_api_key",
|
||||
"openrouter_api_key",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Anthropic's identity-linked keys (personal and service-account) can span workspaces, and such a
|
||||
key must send `anthropic-workspace-id` on every request or the API answers 400 before the model is
|
||||
reached. A user hit exactly that on 2026-09-03 (install 59a37510, three turns) and our card told them
|
||||
to re-enter the key. The workspace id is one setting, sent from the one helper every key lane uses,
|
||||
and the refusal is classified on a declared signal with its own card, first in the auth chain."""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from backend.apps.agents.core.error_classify import is_auth_error, is_workspace_id_error
|
||||
from backend.apps.settings.credentials import anthropic_workspace_header, own_key_anthropic_client, own_key_cli_env
|
||||
from backend.apps.settings.models import AppSettings
|
||||
|
||||
P_REAL_400 = (
|
||||
'The agent runtime reported this turn failed (stop_sequence). API Error: 400 {"type":"error","error":'
|
||||
'{"type":"invalid_request_error","message":"anthropic-workspace-id is required when authenticating with an '
|
||||
'identity-linked API key; send the id of the workspace this request acts in."},"request_id":null}'
|
||||
)
|
||||
P_REAL_404 = 'API Error: 404 {"type":"error","error":{"type":"not_found_error","message":"Workspace `wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ` not found."}}'
|
||||
P_REAL_BAD = 'API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"anthropic-workspace-id header must be a valid workspace ID."}}'
|
||||
|
||||
|
||||
def test_a_key_without_a_workspace_id_gets_exactly_the_env_it_always_did():
|
||||
settings = AppSettings(anthropic_api_key="sk-ant-test")
|
||||
assert own_key_cli_env(settings) == {"ANTHROPIC_API_KEY": "sk-ant-test"}
|
||||
assert anthropic_workspace_header(settings) == {}
|
||||
|
||||
|
||||
def test_a_workspace_id_rides_the_cli_custom_headers_and_the_sdk_default_headers():
|
||||
settings = AppSettings(anthropic_api_key="sk-ant-test", anthropic_workspace_id=" wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ ")
|
||||
env = own_key_cli_env(settings)
|
||||
assert env["ANTHROPIC_API_KEY"] == "sk-ant-test"
|
||||
assert env["ANTHROPIC_CUSTOM_HEADERS"] == "anthropic-workspace-id: wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ"
|
||||
client = own_key_anthropic_client(settings)
|
||||
assert client.default_headers["anthropic-workspace-id"] == "wrkspc_01JwQvzr7rXLA5AGx3HKfFUJ"
|
||||
|
||||
|
||||
def test_anthropics_three_workspace_refusals_are_classified_as_auth_on_a_declared_signal():
|
||||
for text in (P_REAL_400, P_REAL_404, P_REAL_BAD):
|
||||
assert is_workspace_id_error(RuntimeError(text)), text
|
||||
assert is_auth_error(RuntimeError(text)), text
|
||||
# Controls: an ordinary 400, a stray "workspace" and a schema 400 stay what they were.
|
||||
for text in ("line 400, in run", "the workspace is on the left", 'API Error: 400 {"message":"tools.0.input_schema: unsupported"}'):
|
||||
assert not is_workspace_id_error(RuntimeError(text)), text
|
||||
|
||||
|
||||
def test_the_workspace_card_is_decided_before_every_other_auth_reading():
|
||||
src = open(os.path.join(os.path.dirname(__file__), "..", "apps", "agents", "manager", "run", "handle_run_error.py")).read()
|
||||
workspace = src.index("is_workspace_id_error(e, extra_text=p_stderr_tail)")
|
||||
assert workspace < src.index('reason = "codex_token_rotating"')
|
||||
assert workspace < src.index('reason = "anthropic_auth_invalid"')
|
||||
assert 'reason = "anthropic_workspace_id"' in src
|
||||
|
||||
|
||||
def test_no_lane_builds_a_client_or_env_from_the_raw_key_outside_credentials():
|
||||
"""The class seal: the user's own key reaches the wire through own_key_* only, so a lane added
|
||||
tomorrow cannot forget the workspace header the way three lanes had until 2026-09-03."""
|
||||
root = os.path.join(os.path.dirname(__file__), "..", "apps")
|
||||
raw_client = re.compile(r"AsyncAnthropic\(\s*api_key=(?:global_)?settings\.anthropic_api_key")
|
||||
raw_env = re.compile(r'"ANTHROPIC_API_KEY":\s*(?:global_)?settings\.anthropic_api_key')
|
||||
offenders = []
|
||||
for dirpath, _dirs, files in os.walk(root):
|
||||
for name in files:
|
||||
if not name.endswith(".py"):
|
||||
continue
|
||||
path = os.path.join(dirpath, name)
|
||||
text = open(path, encoding="utf-8", errors="replace").read()
|
||||
if path.endswith(os.path.join("settings", "credentials.py")):
|
||||
continue
|
||||
if raw_client.search(text) or raw_env.search(text):
|
||||
offenders.append(os.path.relpath(path, root))
|
||||
assert offenders == [], offenders
|
||||
proxy = open(os.path.join(root, "agents", "proxy", "anthropic_proxy.py")).read()
|
||||
assert "anthropic_workspace_header" in proxy
|
||||
@@ -1874,13 +1874,15 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
{session.context_overflow && (() => {
|
||||
const reason = session.context_overflow.reason;
|
||||
const isAuth = reason === 'openswarm_pro_auth_expired' || reason === 'anthropic_auth_invalid' || reason === 'auth_error';
|
||||
const isWorkspace = reason === 'anthropic_workspace_id';
|
||||
const isOutOfTokens = reason === 'out_of_tokens';
|
||||
const isFreeTrial = reason === 'free_trial_exhausted';
|
||||
const isOutOfCredits = reason === 'out_of_credits';
|
||||
const opensSettings = isAuth || isFreeTrial || isOutOfCredits;
|
||||
const opensSettings = isAuth || isWorkspace || isFreeTrial || isOutOfCredits;
|
||||
const title = isOutOfTokens ? 'Out of tokens'
|
||||
: isFreeTrial ? 'Free runs used up'
|
||||
: isOutOfCredits ? 'Out of credits'
|
||||
: isWorkspace ? 'Workspace ID needed'
|
||||
: isAuth ? 'Sign-in required' : 'Context full';
|
||||
const primaryLabel = isOutOfTokens ? 'Got it'
|
||||
: isFreeTrial ? 'Connect a model'
|
||||
|
||||
@@ -77,6 +77,22 @@ const ApiKeyCard: React.FC<{
|
||||
Get key <OpenInNewIcon sx={{ fontSize: 11 }} />
|
||||
</Typography>
|
||||
</Box>
|
||||
{config.field === 'anthropic_api_key' && value ? (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<TextField
|
||||
value={form.anthropic_workspace_id ?? ''}
|
||||
onChange={(e) => setForm({ ...form, anthropic_workspace_id: e.target.value.trim() || null })}
|
||||
size="small"
|
||||
fullWidth
|
||||
placeholder="Workspace ID (wrkspc_...)"
|
||||
inputProps={{ 'aria-label': 'Anthropic workspace ID' }}
|
||||
sx={{ ...fieldSx, '& .MuiOutlinedInput-root': { ...fieldSx['& .MuiOutlinedInput-root'], fontFamily: c.font.mono } }}
|
||||
/>
|
||||
<Typography sx={{ ...descSx, mt: 0.5 }}>
|
||||
Only for a key that can act in more than one workspace. Find the ID under Settings, Workspaces in the Claude Console.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Anthropic's identity-linked keys can span workspaces and then need a workspace id on every
|
||||
// request. The field lives on the Anthropic card only, appears once a key is present, and the
|
||||
// chat card for that refusal opens Settings under its own title instead of "Sign-in required".
|
||||
|
||||
const card = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/Settings/sections/models/ApiKeyCard.tsx'), 'utf8');
|
||||
const chat = fs.readFileSync(path.join(process.cwd(), 'src/app/pages/AgentChat/AgentChat.tsx'), 'utf8');
|
||||
const slice = fs.readFileSync(path.join(process.cwd(), 'src/shared/state/settingsSlice.ts'), 'utf8');
|
||||
|
||||
test('the workspace id field is gated on the Anthropic card with a key present', () => {
|
||||
assert.match(card, /config\.field === 'anthropic_api_key' && value \? \(/);
|
||||
assert.match(card, /anthropic_workspace_id: e\.target\.value\.trim\(\) \|\| null/);
|
||||
});
|
||||
|
||||
test('the settings shape carries the field with a null default', () => {
|
||||
assert.match(slice, /anthropic_workspace_id: string \| null;/);
|
||||
assert.match(slice, /anthropic_workspace_id: null,/);
|
||||
});
|
||||
|
||||
test('the workspace refusal opens Settings under its own title', () => {
|
||||
assert.match(chat, /const isWorkspace = reason === 'anthropic_workspace_id';/);
|
||||
assert.match(chat, /opensSettings = isAuth \|\| isWorkspace/);
|
||||
assert.match(chat, /isWorkspace \? 'Workspace ID needed'/);
|
||||
});
|
||||
@@ -41,6 +41,7 @@ export interface AppSettings {
|
||||
dictation_sound_volume?: number;
|
||||
dictation_disabled_surfaces?: string;
|
||||
anthropic_api_key: string | null;
|
||||
anthropic_workspace_id: string | null;
|
||||
openai_api_key?: string | null;
|
||||
google_api_key?: string | null;
|
||||
openrouter_api_key?: string | null;
|
||||
@@ -185,6 +186,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
|
||||
dictation_sound_volume: 0.7,
|
||||
dictation_disabled_surfaces: '',
|
||||
anthropic_api_key: null,
|
||||
anthropic_workspace_id: null,
|
||||
browser_homepage: 'https://duckduckgo.com',
|
||||
browser_import_signins: false,
|
||||
auto_select_mode_on_new_agent: false,
|
||||
|
||||
Reference in New Issue
Block a user