Macotron

macotron.ai

Send prompts to Anthropic Claude, OpenAI, Google Gemini, or the on-device Apple Intelligence model, and get the reply as a string or as a stream of chunks.

const opts = macotron.plugin({
  title: "Summarize",
  options: {
    apiKey: { type: "password", label: "Anthropic API key", required: true },
  },
});

const ai = macotron.ai.claude({ apiKey: opts.apiKey, model: "claude-sonnet-5-5" });
const summary = await ai.chat(macotron.clipboard.text(), {
  system: "Summarize the text in one sentence.",
});
macotron.notify.toast(summary);

Providers

Each factory returns a client object with chat() and stream(). Creating a client does no network work. The key and model you pass are stored on the client and used for every request it makes.

FactoryServiceDefault model
claude(), anthropic()Anthropic Messages APIclaude-opus-5-5
openai()OpenAI Chat Completions APIgpt-4o
gemini()Google Gemini APIgemini-2.5-flash
local()Apple Foundation Models, on this MacThe system language model

claude(opts?: { model?: string; apiKey?: string }): AIClient

A client for api.anthropic.com. model is any Anthropic model id. Omit it to use claude-opus-5-5.

anthropic(opts?: { model?: string; apiKey?: string }): AIClient

The same function as claude(), under another name.

openai(opts?: { model?: string; apiKey?: string }): AIClient

A client for api.openai.com. Omit model to use gpt-4o.

gemini(opts?: { model?: string; apiKey?: string }): AIClient

A client for generativelanguage.googleapis.com. Omit model to use gemini-2.5-flash.

local(): AIClient

A client for the on-device Apple Intelligence model. It takes no key and no model, and nothing leaves the Mac.

The local model needs macOS 26, a Mac that can run Apple Intelligence, and Apple Intelligence turned on in System Settings. Otherwise the request rejects with the reason, for example Provider not available: Turn on Apple Intelligence in System Settings. or …The on-device model is still downloading.

API keys

Macotron has no global AI key setting. A cloud client uses only the apiKey you pass to its factory. Without one, every request rejects with API key not provided. Set it in your config.

Keep keys out of plugin source. Declare a password option in macotron.plugin(): the user enters the key in Settings, Macotron stores it in the Keychain, and the option value you get back is the key itself. macotron.keychain also works, and keeps each plugin's keys apart.

The client

The first argument to both methods is a prompt string, or an array of { role, content } messages for a conversation. role is "user" or "assistant". Macotron does not keep chat history: pass the whole conversation on every call, and save it yourself in localStorage if you need it later.

An empty array, an empty content, or any other role throws a TypeError right away, before any request goes out.

OptionDefaultMeaning
systemnoneThe system prompt.
modelThe client's modelOverrides the model for this one request.
maxTokens4096The longest reply, in tokens.
temperatureprovider defaultSampling temperature. Leave it out for current Claude models, which reject it.
onChunknonestream() only. Called with each new piece of text.

The local model reads only system. It ignores model, maxTokens, and temperature.

chat(promptOrMessages: string | AIChatMessage[], opts?): Promise<string>

Sends the request and resolves with the full reply text. Cloud requests time out after 120 seconds.

The promise rejects with a message when the request fails: HTTP 401: … with the provider's error body for a refused request, Network error: … when the network is down, or the key and availability errors above.

stream(promptOrMessages: string | AIChatMessage[], opts?): Promise<string>

Calls onChunk with each piece of text as it arrives, then resolves with the full reply. Each chunk is only the new text, so append it to what you already have. Errors reject the same way as chat().

const ai = macotron.ai.local();
let text = "";
const full = await ai.stream("Write a haiku about Mondays.", {
  onChunk: (chunk) => {
    text += chunk;
    console.log(text);
  },
});
macotron.notify.show("Haiku", full);

When the plugin reloads, any request still in flight is dropped and onChunk is not called again.

Text you send to a model can carry instructions of its own. When a prompt includes screen text, clipboard contents, or file contents, wrap that data in clear delimiters and tell the model to ignore instructions inside it.

See also