Quickstart
Install MMSP, stream a first response, then let the model call a tool.
Install
npm install @prismshadow/mmsp
pip install mmsp
The TypeScript package needs Node.js 20 or newer.
The Python package needs Python 3.11 or newer. uv add mmsp works as well.
Set the API key
MMSP reads the provider's key from the environment: ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, DEEPSEEK_API_KEY, MOONSHOT_API_KEY, ZAI_API_KEY or MINIMAX_API_KEY. You can also pass api_key and base_url to the client. The examples below name a model whose id picks its official client; Models and endpoints has every client and model.
Stream a response
import { AutoLLMClient } from "@prismshadow/mmsp";
const client = new AutoLLMClient({ model: "gpt-6.1-sol" });
for await (const event of client.streamingResponseStateful({
message: {
role: "user",
content_items: [{ type: "text.done", text: "Say 'Hello, World!'" }],
},
config: {},
})) {
console.log(event);
}
import asyncio
from mmsp import AutoLLMClient
async def main():
client = AutoLLMClient(model="gpt-6.1-sol")
async for event in client.streaming_response_stateful(
message={
"role": "user",
"content_items": [{"type": "text.done", "text": "Say 'Hello, World!'"}],
},
config={},
):
print(event)
asyncio.run(main())
The stream prints four fragments, the complete text, then the one event that ends it:
{"role": "assistant", "event_type": "delta", "content_items": [{"type": "text.delta", "text": "Hello"}], "usage_metadata": null, "finish_reason": null}
{"role": "assistant", "event_type": "delta", "content_items": [{"type": "text.delta", "text": ","}], "usage_metadata": null, "finish_reason": null}
{"role": "assistant", "event_type": "delta", "content_items": [{"type": "text.delta", "text": " World"}], "usage_metadata": null, "finish_reason": null}
{"role": "assistant", "event_type": "delta", "content_items": [{"type": "text.delta", "text": "!"}], "usage_metadata": null, "finish_reason": null}
{"role": "assistant", "event_type": "delta", "content_items": [{"type": "text.done", "text": "Hello, World!"}], "usage_metadata": null, "finish_reason": null}
{"role": "assistant", "event_type": "stop", "content_items": [], "usage_metadata": {"cached_tokens": 0, "prompt_tokens": 12, "thoughts_tokens": 0, "response_tokens": 8}, "finish_reason": "stop"}
Change the model id to claude-opus-5-5 or gemini-3.8-flash and the same code prints the same shapes.
Call a tool
The model asks for a tool with a tool_call.done item. Run the tool, then send its result back with the same tool_call_id.
import { AutoLLMClient, ToolCallDoneItem } from "@prismshadow/mmsp";
const TOOLS: Record<string, (args: Record<string, any>) => string> = {
get_weather: (args) => `Temperature in ${args.location}: 22 C`,
};
const weatherTool = {
name: "get_weather",
description: "Gets the current weather for a given location.",
parameters: {
type: "object" as const,
properties: { location: { type: "string" as const, description: "The city name" } },
required: ["location"],
},
};
const client = new AutoLLMClient({ model: "gpt-5.5" });
const config = { tools: [weatherTool] };
let toolCall: ToolCallDoneItem | null = null;
for await (const event of client.streamingResponseStateful({
message: {
role: "user",
content_items: [{ type: "text.done", text: "What's the weather in London?" }],
},
config,
})) {
for (const item of event.content_items) {
if (item.type === "tool_call.done") {
toolCall = item;
}
}
}
if (toolCall) {
const result = TOOLS[toolCall.name](toolCall.arguments);
for await (const event of client.streamingResponseStateful({
message: {
role: "user",
content_items: [
{ type: "tool_result.done", text: result, tool_call_id: toolCall.tool_call_id },
],
},
config,
})) {
console.log(event);
}
}
import asyncio
from mmsp import AutoLLMClient
def get_weather(location: str) -> str:
return f"Temperature in {location}: 22 C"
TOOLS = {"get_weather": get_weather}
weather_tool = {
"name": "get_weather",
"description": "Gets the current weather for a given location.",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string", "description": "The city name"}},
"required": ["location"],
},
}
async def main():
client = AutoLLMClient(model="gpt-5.5")
config = {"tools": [weather_tool]}
tool_call = None
async for event in client.streaming_response_stateful(
message={
"role": "user",
"content_items": [{"type": "text.done", "text": "What's the weather in London?"}],
},
config=config,
):
for item in event["content_items"]:
if item["type"] == "tool_call.done":
tool_call = item
if tool_call:
result = TOOLS[tool_call["name"]](**tool_call["arguments"])
async for event in client.streaming_response_stateful(
message={
"role": "user",
"content_items": [
{
"type": "tool_result.done",
"text": result,
"tool_call_id": tool_call["tool_call_id"],
}
],
},
config=config,
):
print(event)
asyncio.run(main())
Rules for an agent loop
The agent loop turns these into a complete implementation.
- Read tool calls from
tool_call.doneitems.tool_call.deltaitems are fragments of the arguments, good for showing progress and nothing else. - Send every tool result with the exact
tool_call_idof itstool_call.done. - Read the usage and the finish reason from the
stopevent. It is always the last event and arrives exactly once. - Keep
thinking.doneitems and everyfidelityfield as they arrived. The stateful call does this for you. - When the arguments of a call do not parse, MMSP raises
ToolCallArgumentParseErrorin place of thetool_call.done. Retry or re-prompt; never run a tool from partial arguments.