Tier 2 · AI Core
API Reference

AI provider backend

core/ai/ — the multi-provider backend underneath the AI Companion panel. If ClipsAPI is "what the AI can do to a clip," this is "how a request actually reaches a model and comes back as a proposal" — the layer below ai_panel.py and execute.py.

Shape of a request/response

core/ai/schemas.py defines the one shape every provider client returns:

@dataclass
class AIResponse:
    explanation: str = ""
    python_code: str = ""
    error: str = ""

    @property
    def ok(self):
        return not self.error

error is set (with explanation/python_code left blank) only when the request itself failed — network, auth, or malformed model output. Callers must check error/ok first and never assume python_code is present otherwise.

The provider registry

core/ai/providers.py's PROVIDERS dict is the single source of truth for what providers exist:

idLabelDefault modelpip package
"anthropic"Anthropic (Claude)claude-sonnet-5anthropic
"openai"OpenAI (GPT)gpt-5.5openai
"gemini"Google (Gemini)gemini-pro-latestgoogle-genai

DEFAULT_PROVIDER = "anthropic".

build_client(provider=None, model=None) -> AIClient

Falls back to DEFAULT_PROVIDER if provider is falsy; raises ValueError for an unknown provider id. Instantiates the concrete client class with model=model or entry["default_model"] — no API key is passed here; each client resolves its own key from key storage unless constructed with one directly.

Other functions in this module, mostly consumed by Settings > AI:

None of the three SDKs ship with Maya's bundled Python. Settings > AI's Install button is the intended install path, specifically via mayapy -m pip install ... — a user's system Python isn't relevant here (sys.executable inside interactive Maya resolves to maya.exe, not mayapy.exe).

The client interface

core/ai/client_base.py — the entire abstract surface is one method:

class AIClient:
    def generate_code(self, user_request, context=""):
        raise NotImplementedError

context is a plain-text summary the caller assembles (current selection, a domain-specific "concepts primer" — see ai_panel.py's context_provider hook, documented in AI Companion). The client folds it into the prompt; it never fetches anything itself.

Each concrete client shares the same structural pattern:

Provider-specific notes

A

Anthropic (anthropic_client.py)

messages.create(..., system=system_prompt_for_scope(), tools=[...], tool_choice={"type": "tool", "name": "propose_script"}). The system prompt goes in via a dedicated system= kwarg. Tool input arrives already-decoded (a dict, no manual JSON parsing needed) — the SDK guarantees well-formed output against the declared schema via constrained decoding.

O

OpenAI (openai_client.py)

Chat Completions API (not the newer Responses API). The system prompt goes in as a "system"-role message in the messages list, not a dedicated kwarg — the one structural difference from Anthropic worth remembering. Unlike Anthropic, function arguments come back as a raw JSON string (call.function.arguments), requiring an explicit json.loads(...).

G

Gemini (gemini_client.py)

Wraps the unified google-genai package (not the older, now-superseded google-generativeai package). Default model is deliberately the alias gemini-pro-latest rather than a dated concrete id, after a sibling tool once hardcoded a specific Gemini model id that later 404'd once Google moved on. System prompt goes in via GenerateContentConfig(system_instruction=...); tool calling is forced via ToolConfig(function_calling_config=FunctionCallingConfig(mode="ANY", allowed_function_names=["propose_script"])).

Each client's fallback path (if the model somehow doesn't return the forced tool call — "should be unreachable" per the code's own comments) returns whatever free text the model did produce as an AIResponse with error set, rather than crashing on a missing field.

The system prompt

core/ai/prompts.pySYSTEM_PROMPT is sent (via system_prompt_for_scope(), currently a pass-through, kept as a hook for a future narrower per-domain scope) with every request, alongside build_user_message(user_request, context) (prefixes f"Current context:\n{context}\n\n" when context is non-empty).

What it instructs, in summary:

This is prompt-level safety framing, not a sandboxing mechanism — see AI Companion's safety model for what actually enforces the boundary (a human reading the code before clicking Run).

API key storage

core/ai/key_storage.py — keys live in the OS keychain via the keyring package, never in UserConfig's plain registry-backed store (unlike Perforce credentials, which do live there in plaintext — this is a deliberately stronger precedent for anything that's an actual secret). Namespaced per-provider (f"{provider}_api_key") under a service name distinct from any other CoreTools-family tool sharing the same machine, so switching provider never clobbers another provider's stored key, and two different tools' keys never collide.

keyring_available() -> bool
get_api_key(provider="anthropic") -> str | None
set_api_key(provider, value) -> bool
clear_api_key(provider="anthropic") -> bool

Every function degrades to a safe default (False/None) rather than raising if keyring isn't importable — Maya's bundled Python doesn't ship it by default. Settings > AI disables the key field entirely and shows an install hint (mayapy -m pip install keyring) when keyring_available() is false.