Skip to content

PolarGrid Provider

PolarGrid is a Canadian-hosted inference network with regional edges in Toronto, Vancouver, and Montreal. Use it when data residency on Canadian soil matters. It serves OpenAI-shaped chat completions with streaming and tool / function calling; voice is limited (see below).

Install

pip install roomkit[polargrid]   # requires polargrid-sdk>=0.10.0

Quick start

from roomkit import AIChannel, RoomKit
from roomkit.providers.polargrid import PolarGridAIProvider, PolarGridConfig

provider = PolarGridAIProvider(
    PolarGridConfig(
        api_key="pg_...",          # from the PolarGrid Console
        model="qwen-3.8-27b",      # default; qwen-3.6-35b-a3b on yul-02
        region=None,               # None = auto-route; pin in production
    )
)

kit = RoomKit()
kit.register_channel(AIChannel("ai", provider=provider))

A full runnable example lives at examples/polargrid_ai.py.

Configuration

Field Default Notes
api_key (required) pg_... Bearer token from the PolarGrid Console
model qwen-3.8-27b The one LLM on the public fleet. qwen-3.5-27b was retired on 2026-08-20; qwen-3.6-35b-a3b (vision) is a customer pilot with no public edge. See Models — or call PolarGridAIProvider.available_models() / list_models().
region None toronto / vancouver / montreal — or the IDs yto-01 / yvr-02 / yul-01. None auto-routes to an edge already serving the configured model.
max_tokens None At least 1. Sent as 4096 (PolarGrid's documented maximum, which polargrid-sdk enforces), or what the model's window leaves after the prompt, when None: the SDK's own default, 150, cuts an answer mid-sentence under a stop finish. A larger value is sent as 4096, with one warning.
top_p 0.9 0.0-1.0
thinking None Toggle qwen reasoning via the enable_thinking request flag (sdk 0.8.5+) — True on, False off, None leaves it unset. See Thinking / reasoning.
timeout 30.0 Seconds.
connect_timeout 5.0 Seconds, TCP connect alone. timeout stays the read budget, so a dead edge is refused in seconds rather than after it. See Connect vs Read Timeout.
max_retries 0 Defaults to 0 so RoomKit's RetryPolicy controls retries.
debug False Verbose SDK logging.

Regions and data residency

PolarGrid runs three Canadian edges. Two ways to choose one:

# Auto-routing — on first call, the autorouter picks the nearest edge
# that already has the configured model loaded (polargrid-sdk 0.10.0);
# if no edge serves it, routing falls back to the default edge.
config = PolarGridConfig(api_key="pg_...", region=None)

# Pinned — request always goes to the named edge.
config = PolarGridConfig(api_key="pg_...", region="vancouver")

The auto-routing path is convenient for development but pin a region in production if residency matters. Confirm with PolarGrid whether their auto-routing or failover ever crosses regions before relying on it for compliance.

Region discovery

available_regions() is the curated, offline catalog of all PolarGrid edges (id, name, location); connected_region() reports the edge a provider is actually routed to. Both return PolarGridRegion, and location carries the Canada / US split that matters for residency:

from roomkit.providers.polargrid import PolarGridAIProvider

# All edges, filtered to the Canadian ones (Law 25 / PIPEDA).
canadian = [r for r in PolarGridAIProvider.available_regions()
            if (r.location or "").startswith("Canada")]
# → yto-01 Toronto, yul-01 Montreal, yul-02 Montreal 02 (pilot), yvr-02 Vancouver

# Which edge am I actually hitting (esp. under auto-routing)?
here = await provider.connected_region()
print(here.id, here.name, here.location)   # e.g. "yul-01" "Montreal" "Canada East"
Region Name Location
yto-01 Toronto Canada Central
yul-01 Montreal Canada East
yul-02 Montreal 02 Canada East (customer-pilot edge, no longer in the published list)
yvr-02 Vancouver Canada West
nyc-01 / nyc-02 / was-01 / mia-01 New York, Washington DC, Miami US East
dfw-01 / dfw-02 / chi-01 Dallas, Chicago US Central
sfo-01 / sfo-03 / lax-01 / sea-01 / phx-01 San Francisco, Los Angeles, Seattle, Phoenix US West

available_regions() mirrors the SDK's own routing table (16 edges in polargrid-sdk 0.10.0). PolarGrid's regions guide publishes 15 of them: yul-02 left the public fleet with the qwen-3.6-35b-a3b pilot, but the SDK still routes it and the edge still answers, so pinning it is not refused. There is no live full-region endpoint (/v1/status 404s on edges), so connected_region() reports only the routed edge (its location is backfilled from the catalog). Every edge answered /health on 2026-09-02 except yto-01, down at the time.

Models

Like every RoomKit AI provider, PolarGrid exposes two discovery entry points returning ModelInfo:

# Curated, offline catalog — a classmethod, no API key or network.
for m in PolarGridAIProvider.available_models():
    print(m.id, m.display_name, m.capabilities)

# Live query against the connected edge (region-specific).
provider = PolarGridAIProvider(PolarGridConfig(api_key="pg_...", region="yul-01"))
for m in await provider.list_models():
    print(m.id)

available_models() is the curated snapshot of the chat models on the public edges (sourced from PolarGrid's model pages and model availability guide, cross-checked against the autorouter on 2026-09-02); list_models() returns whatever is actually loaded on the connected edge, including the STT/TTS models. The edge reports no model type, so the provider tags each model from its catalog (a chat model's completion, tools, thinking…; transcription on whisper-large-v3-turbo and cohere-transcribe-03-2026; speech on kokoro-82m and tada-3b-ml) and backfills display names, context windows and vision flags, so a model picker can keep the speech models out of a chat list. An id the catalog does not know is tagged by its name when it names a speech model (whisper, transcribe, tts), else comes with no capabilities: unknown, not none.

Model Type Availability
qwen-3.8-27b chat (tools, thinking), 256K context, $0.20 / $0.75 per 1M tokens fleet-wide (every public edge)
qwen-3.6-35b-a3b chat (tools, thinking, vision), 8K served context customer pilot: no public edge; recognised (supports_vision, list_models backfill) but not advertised
qwen-3.5-27b chat retired 2026-08-20: 404 model_not_loaded everywhere, dropped from the catalog
whisper-large-v3-turbo STT, tagged transcription all edges (dfw-02 included, checked 2026-10-02)
cohere-transcribe-03-2026 STT, tagged transcription all edges (dfw-02 included, checked 2026-10-02)
kokoro-82m TTS, tagged speech all edges (dfw-02 included, checked 2026-10-02)
tada-3b-ml TTS, tagged speech all edges

The autorouter answers GET https://autorouter.polargrid.ai/v1/route?model=<id> with 404 when no edge serves an id, which is how the catalog is checked without a key. The default qwen-3.8-27b reasons via enable_thinking. See examples/list_models.py for a runnable catalog dump across providers.

Streaming

PolarGrid streams via OpenAI-shaped chunked SSE. RoomKit's provider exposes both plain text deltas and structured events.

A streamed turn reports its usage on its StreamDone, like a non-streamed one. polargrid-sdk can neither ask for a stream's usage nor read it (no stream_options on its request, no usage on its chunk, as of 0.10.0), while PolarGrid's server sends it last when asked. Until the SDK carries it, the provider streams through a patch that asks for it and reads it, the SDK still building and authenticating the request (roomkit.providers.polargrid.sdk_patch). The counts are the server's: its prompt_tokens appears to leave out the chat template and the declared tools.

async for delta in provider.generate_stream(context):
    print(delta, end="", flush=True)

async for event in provider.generate_structured_stream(context):
    # StreamThinkingDelta | StreamTextDelta | StreamToolCall | StreamDone
    ...

Thinking / reasoning

qwen surfaces its reasoning inline as <think>...</think> tags in the message content (the same convention vLLM / Ollama reasoning models use; PolarGrid has no separate reasoning_content field). The provider parses those tags out so:

  • non-streaming generate() returns the reasoning on AIResponse.thinking and the answer without its tags on AIResponse.content, exactly as the stream splits them (spaces kept);
  • streaming generate_structured_stream() emits the reasoning as StreamThinkingDelta (handling tags split across chunks) and the answer as StreamTextDelta.

generate_stream() (plain text) filters thinking out entirely.

Reasoning is off by default on the edge. polargrid-sdk 0.8.5+ exposes an enable_thinking request flag, which the provider sets from the thinking config:

PolarGridConfig(api_key="pg_...", thinking=True)   # enable_thinking=true  → reasoning on
PolarGridConfig(api_key="pg_...", thinking=False)  # enable_thinking=false → reasoning off
PolarGridConfig(api_key="pg_...")                  # thinking=None         → flag unset (model default)

A turn outranks the configured switch (RFC §6.7): thinking_budget (0 off, above 0 on), enable_thinking, or a reasoning_effort of "none" sets the flag for that turn. qwen has no effort tier, so any other reasoning_effort leaves it as configured.

Thinking responses are larger and slower (the reasoning counts toward latency and max_tokens), so raise timeout when enabling it, and max_tokens if you set it below 4096. To display reasoning in a CLI, construct the channel with CLIChannel("cli", show_thinking=True).

Tool / function calling

PolarGrid's chat-completions endpoint supports tool / function calling as of polargrid-sdk>=0.8.5. The provider forwards context.tools (OpenAI-shaped) and surfaces tool calls back:

  • Non-streaming — generate() returns them on AIResponse.tool_calls.
  • Streaming — generate_structured_stream() emits a StreamToolCall per call after the text deltas, accumulating the SDK's fragmented delta.tool_calls.

PolarGrid sends tool arguments as a JSON string; the provider parses them into a dict for RoomKit (malformed payloads are preserved under a raw key). For multi-turn tool loops, assistant tool calls and tool results are rendered back into structured messages (role="assistant" with tool_calls, role="tool" with tool_call_id) rather than flattened to text.

from roomkit.providers.ai.base import AITool

context.tools = [
    AITool(
        name="get_weather",
        description="Get current weather for a city.",
        parameters={
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    )
]

tool_choice is not exposed by AIContext, so it is left unset and the backend defaults to auto. Forcing a specific tool is steered, not hard-guaranteed, on PolarGrid's backend — design tool loops to tolerate the model answering directly instead of calling the tool.

examples/polargrid_ai.py wires a web_search tool end-to-end: it passes tools=[WebSearchTool()] to the AIChannel, which runs the whole loop (model → web_search → grounded answer). Ask it "What is the speed of light?" to see the model search and answer from the result. The tool works key-free (Wikipedia search + summary, which handles the natural-language queries models generate); set TAVILY_API_KEY for real web search that also finds niche companies and current info.

Vision

PolarGrid added multimodal chat in polargrid-sdk>=0.9.0: Message.content now accepts OpenAI-shaped image_url parts. The provider renders an AIImagePart (in a user turn or split off an image tool result) as an image_url block — the URL may be a remote https:// URL or a base64 data: URI.

supports_vision is model-driven: it reads the configured model's flag from the curated catalog, pilot models included. Only qwen-3.6-35b-a3b actually reads images — verified live on yul-02 while that edge was public. The default qwen-3.8-27b refuses image input outright (ValidationError: "does not support image input"), so it — and any unknown model id — is treated as text-only. Vision is the deployed model's capability, not the SDK's, and as of 2026-09-02 no public edge serves a vision-capable model: qwen-3.6-35b-a3b is a customer pilot.

With pilot access, pin the vision model and the pilot edge you were given:

PolarGridConfig(api_key="pg_...", model="qwen-3.6-35b-a3b", region="yul-02")

examples/polargrid_ai.py accepts a /image <path> [question] command: it embeds the local file as a base64 data: URI and sends it as an image_url part (defaulting to "Analyse this image." when no question is given). Run it with POLARGRID_MODEL=qwen-3.6-35b-a3b POLARGRID_REGION=<pilot edge> for vision.

Error handling

The provider maps the PolarGrid SDK's exception hierarchy onto RoomKit's ProviderError, with the HTTP status the SDK built each exception from (only ServerError carries its own):

PolarGrid exception retryable status_code
AuthenticationError False 401 (the SDK folds 403 into it too)
BillingError False 402
ValidationError False 400
NotFoundError False 404
RateLimitError True 429
NetworkError with a transport error under it True none
NetworkError for an error written into a stream False (the SDK drops its status) none
TimeoutError True none
ServerError True its own (5xx)
unknown (a 408 or 409 among them: the SDK drops the status) False, unless its message names a retried status, a rate limit or an overload none

Roadmap

PolarGrid also exposes Speech-to-Text (whisper-large-v3-turbo) and Text-to-Speech (kokoro-82m) endpoints. RoomKit STT/TTS provider wrappers are planned. There is no speech-to-speech / realtime duplex endpoint today, so RealtimeVoiceChannel is out of scope for this provider.