From 54ee734ab51b3ba1cd3313735087eb4e4a1e3d0c Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 8 Aug 2026 20:09:19 +0300 Subject: [PATCH] providers: presets for polza/openai/ollama (--provider), keys from env, gpt-5.6-luna default for polza, multimodal heuristic for cloud ids --- .../console-2026-08-08T09-11-01-487Z.log | 53 +++++++++++ testbed/chat.py | 41 ++++++-- testbed/demo.py | 36 ++++++- testbed/llm_agent.py | 93 ++++++++++++++++++- 4 files changed, 211 insertions(+), 12 deletions(-) diff --git a/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log b/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log index 700383e..70f07fb 100644 --- a/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log +++ b/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log @@ -7505,3 +7505,56 @@ [28512300ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 [28514857ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 [28518619ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28523522ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28529061ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28531079ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28533322ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28538120ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28540727ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28546195ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28549179ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28554313ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28559812ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28565254ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28568642ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28571765ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28575218ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28579019ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28581688ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28586866ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28590751ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28591753ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28592754ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28593756ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28594757ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28595761ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28596765ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28597770ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28598784ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28599809ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28600846ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28601947ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28603206ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28604456ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28606503ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28609814ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28612957ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28617768ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28621897ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28626310ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28630569ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28635095ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28640867ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28644270ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28649497ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28654991ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28660090ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28662946ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28665487ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28669449ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28674591ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28679193ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28683363ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28686271ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28688761ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[28694226ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 diff --git a/testbed/chat.py b/testbed/chat.py index 96fde11..e9ce41f 100644 --- a/testbed/chat.py +++ b/testbed/chat.py @@ -32,11 +32,14 @@ import sys from aicc.client import AICCClient from aicc.transport.websocket import WebSocketClientTransport -from testbed.llm_agent import CHAT_MISSION, LLMController, ollama_models +from testbed.llm_agent import ( + CHAT_MISSION, + LLMController, + ollama_models, + resolve_provider, +) DEFAULT_URL = "ws://127.0.0.1:8765" -DEFAULT_BASE_URL = "http://localhost:11434/v1" -DEFAULT_MODEL = "gemma4:e2b" HELP = """\ You are talking to the capsule's brain (LLM). Type what you want, e.g.: @@ -393,12 +396,26 @@ def main() -> int: help=f"bridge WebSocket URL (default {DEFAULT_URL})", ) parser.add_argument( - "--base-url", default=DEFAULT_BASE_URL, help="OpenAI-compatible endpoint" + "--base-url", + default=None, + help="OpenAI-compatible endpoint (default per --provider)", ) parser.add_argument( - "--model", default=DEFAULT_MODEL, help="model id on the endpoint" + "--provider", + choices=["polza", "openai", "ollama"], + default=None, + help="provider preset: endpoint + key from env (POLZA_API_KEY/OPENAI_API_KEY)", + ) + parser.add_argument( + "--model", + default=None, + help="model id on the endpoint (default per provider: polza -> openai/gpt-5.6-luna)", + ) + parser.add_argument( + "--api-key", + default=None, + help="API key (default: $POLZA_API_KEY / $OPENAI_API_KEY per provider)", ) - parser.add_argument("--api-key", default="ollama", help="API key for the endpoint") parser.add_argument( "--auto-steps", type=int, @@ -447,6 +464,18 @@ def main() -> int: help="attach a fresh camera frame every N turns (0 disables; default 3)", ) args = parser.parse_args() + try: + args.base_url, args.api_key, args.model = resolve_provider( + provider=args.provider, + base_url=args.base_url, + api_key=args.api_key, + model=args.model, + ) + if args.provider: + print(f"[chat] provider: {args.provider} -> {args.base_url} model: {args.model}") + except ValueError as exc: + print(f"[chat] {exc}") + return 1 try: return asyncio.run(run(args)) except KeyboardInterrupt: diff --git a/testbed/demo.py b/testbed/demo.py index 301a3b9..9ebe218 100644 --- a/testbed/demo.py +++ b/testbed/demo.py @@ -37,9 +37,9 @@ from aicc.protocol import SessionInit from aicc.transport.websocket import WebSocketClientTransport from PIL import Image +from testbed.llm_agent import resolve_provider + DEFAULT_URL = "ws://127.0.0.1:8765" -DEFAULT_BASE_URL = "http://localhost:11434/v1" -DEFAULT_MODEL = "gemma4:e2b" class FrameRecorder: @@ -392,12 +392,26 @@ def main() -> int: help="agent driver (default auto: LLM if reachable, else scripted)", ) parser.add_argument( - "--base-url", default=DEFAULT_BASE_URL, help="OpenAI-compatible endpoint" + "--base-url", + default=None, + help="OpenAI-compatible endpoint (default per --provider)", ) parser.add_argument( - "--model", default=DEFAULT_MODEL, help="model id on the endpoint" + "--provider", + choices=["polza", "openai", "ollama"], + default=None, + help="provider preset: endpoint + key from env (POLZA_API_KEY/OPENAI_API_KEY)", + ) + parser.add_argument( + "--model", + default=None, + help="model id on the endpoint (default per provider: polza -> openai/gpt-5.6-luna)", + ) + parser.add_argument( + "--api-key", + default=None, + help="API key (default: $POLZA_API_KEY / $OPENAI_API_KEY per provider)", ) - parser.add_argument("--api-key", default="ollama", help="API key for the endpoint") parser.add_argument("--max-steps", type=int, default=60, help="max agent steps") parser.add_argument( "--vision", @@ -429,6 +443,18 @@ def main() -> int: "into this directory, plus demo.gif animation and demo_summary.png", ) args = parser.parse_args() + try: + args.base_url, args.api_key, args.model = resolve_provider( + provider=args.provider, + base_url=args.base_url, + api_key=args.api_key, + model=args.model, + ) + if args.provider: + print(f"[demo] provider: {args.provider} -> {args.base_url} model: {args.model}") + except ValueError as exc: + print(f"[demo] {exc}") + return 1 try: summary = asyncio.run(run_demo(args)) except KeyboardInterrupt: diff --git a/testbed/llm_agent.py b/testbed/llm_agent.py index 20f41c4..67304ca 100644 --- a/testbed/llm_agent.py +++ b/testbed/llm_agent.py @@ -67,6 +67,97 @@ NUDGE = ( "then call a tool (turn or move, or interact if within 1.6 m)." ) +# --------------------------------------------------------------------------- +# Provider presets (OpenAI-compatible endpoints) +# --------------------------------------------------------------------------- + +PROVIDERS: dict[str, dict[str, str | None]] = { + "polza": { + "base_url": "https://polza.ai/api/v1", + "env_key": "POLZA_API_KEY", + "default_model": "openai/gpt-5.6-luna", + }, + "openai": { + "base_url": "https://api.openai.com/v1", + "env_key": "OPENAI_API_KEY", + "default_model": "gpt-4o-mini", + }, + "ollama": { + "base_url": "http://localhost:11434/v1", + "env_key": None, + "default_model": "gemma4:e2b", + }, +} + +# Best-effort multimodal detection for non-ollama providers (no way to +# introspect remotely): ids carrying these markers usually accept images. +_MULTIMODAL_HINTS = ( + "gpt-4o", + "gpt-4.1", + "gpt-5", + "gemma", + "gemini", + "claude", + "vl-", + "vision", + "luna", +) + + +def resolve_provider( + *, + provider: str | None, + base_url: str | None, + api_key: str | None, + model: str | None, +) -> tuple[str, str, str]: + """Resolve endpoint + key + model from a provider preset (or explicit args). + + Keys come from the environment (POLZA_API_KEY / OPENAI_API_KEY) so nothing + secret lives in the repo or on the command line. + """ + import os + + if provider: + preset = PROVIDERS.get(provider) + if preset is None: + raise ValueError( + f"unknown provider {provider!r}; available: {', '.join(PROVIDERS)}" + ) + base_url = preset["base_url"] + if model is None: + model = preset["default_model"] + env_key = preset["env_key"] + if api_key is None and env_key: + api_key = os.environ.get(env_key) + if api_key is None: + raise ValueError( + f"provider {provider!r} needs an API key: set ${env_key} " + "(see https://polza.ai/dashboard/api-keys)" + ) + else: + base_url = base_url or str(PROVIDERS["ollama"]["base_url"]) + model = model or str(PROVIDERS["ollama"]["default_model"]) + if api_key is None: + host = base_url.replace("http://", "").replace("https://", "").split("/")[0] + if "polza" in host: + api_key = os.environ.get("POLZA_API_KEY") + if not api_key: + raise ValueError("set $POLZA_API_KEY (polza.ai/dashboard/api-keys)") + elif "openai" in host: + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + raise ValueError("set $OPENAI_API_KEY") + else: + api_key = "ollama" # local ollama ignores the key + return base_url, api_key, model + + +def looks_multimodal(model: str) -> bool: + low = model.lower() + return any(h in low for h in _MULTIMODAL_HINTS) + + COLOR_NAMES = [ ("beacon_cyan", (120, 210, 235)), ("beacon_yellow", (255, 190, 90)), @@ -236,7 +327,7 @@ class LLMController: self.client = client self.model = model if multimodal is None: - multimodal = detect_multimodal(base_url, model) + multimodal = looks_multimodal(model) or detect_multimodal(base_url, model) self.multimodal = multimodal # The color-grid digest is the fallback channel for text-only models. # For multimodal models it is off unless explicitly requested.