@amber-core/sdk-mobile
v0.2.1
Published
Mobile SDK for the Amber platform — typed HTTPS client for LLM proxy, config, streaming, tools, structured output, images.
Maintainers
Readme
@amber-core/sdk-mobile
Typed HTTPS client for the Amber shared platform. Use it from React Native / Expo apps to call the LLM proxy without putting provider keys in the bundle.
v0.2.1 adds llm.images.edit for prompt-driven editing of an existing image. v0.2.0 added tool calling, structured output (llm.object), image input (vision), and image generation (llm.images.generate).
Install
npm install @amber-core/sdk-mobileSetup
import { createPlatformClient } from "@amber-core/sdk-mobile";
const platform = createPlatformClient({
apiUrl: "https://abc.execute-api.us-east-1.amazonaws.com",
llmStreamUrl: "https://xyz.lambda-url.us-east-1.on.aws/",
appId: process.env.EXPO_PUBLIC_PLATFORM_APP_ID!,
getToken: () => clerk.getToken({ template: "platform" }),
});Chat
const reply = await platform.llm.chat({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "hi" }],
estimatedTokens: 100,
});
for await (const chunk of platform.llm.chatStream({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "tell me a story" }],
estimatedTokens: 500,
})) {
if (chunk.type === "delta") process.stdout.write(chunk.content);
}Tools (low-level)
const tools = [
{
name: "get_weather",
description: "Get current weather for a city.",
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
];
const r1 = await platform.llm.chat({
model: "claude-3-5-sonnet-20241022",
messages: [{ role: "user", content: "Weather in Berlin?" }],
tools,
estimatedTokens: 400,
});
if (r1.finishReason === "tool_use") {
const toolUses = r1.blocks.filter((b) => b.type === "tool_use");
// Execute tools, then send results back as a new turn:
const r2 = await platform.llm.chat({
model: "claude-3-5-sonnet-20241022",
tools,
messages: [
{ role: "user", content: "Weather in Berlin?" },
{ role: "assistant", content: r1.blocks },
{
role: "user",
content: toolUses.map((tu) => ({
type: "tool_result",
toolUseId: tu.id,
content: "18°C, cloudy",
})),
},
],
estimatedTokens: 500,
});
console.log(r2.content);
}Tool streaming emits tool_use_start → one or more tool_use_delta (raw JSON fragments) → tool_use_stop (with parsed input):
for await (const c of platform.llm.chatStream({ tools, messages, model, estimatedTokens: 500 })) {
if (c.type === "delta") process.stdout.write(c.content);
else if (c.type === "tool_use_start") console.log("\n→ calling", c.name);
else if (c.type === "tool_use_stop") console.log("input:", c.input);
}Structured output (llm.object)
Pass a JSON Schema and get back a typed object. The backend forces JSON output with both providers (OpenAI native response_format, Anthropic via forced tool — transparent to you).
const { object } = await platform.llm.object<{
city: string;
forecast: { day: string; tempC: number }[];
}>({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "5-day forecast for Berlin in JSON." }],
schemaName: "Forecast",
schema: {
type: "object",
properties: {
city: { type: "string" },
forecast: {
type: "array",
items: {
type: "object",
properties: { day: { type: "string" }, tempC: { type: "number" } },
required: ["day", "tempC"],
},
},
},
required: ["city", "forecast"],
},
estimatedTokens: 600,
});With Zod (optional)
The /zod subpath converts a Zod 4 schema into JSON Schema. Zod is a peer dep — only loaded if you import this subpath.
import { z } from "zod";
import { fromZod } from "@amber-core/sdk-mobile/zod";
const Forecast = z.object({
city: z.string(),
forecast: z.array(z.object({ day: z.string(), tempC: z.number() })),
});
const { object } = await platform.llm.object<z.infer<typeof Forecast>>({
model: "gpt-4o-mini",
messages: [{ role: "user", content: "5-day forecast for Berlin." }],
schema: fromZod(Forecast),
schemaName: "Forecast",
estimatedTokens: 600,
});Vision (image input)
Send images as content blocks. Both URL and base64 sources work with OpenAI; Anthropic accepts base64 only.
import { estimateImageTokens } from "@amber-core/sdk-mobile";
const image = {
type: "image" as const,
source: { type: "url" as const, url: "https://example.com/cat.jpg" },
};
await platform.llm.chat({
model: "gpt-4o",
messages: [
{
role: "user",
content: [
image,
{ type: "text", text: "What breed?" },
],
},
],
estimatedTokens: 400 + estimateImageTokens(image, "openai", { width: 1024, height: 768 }),
});Image generation
const res = await platform.llm.images.generate({
model: "dall-e-3",
prompt: "An isometric studio for a small mobile team",
size: "1024x1024",
responseFormat: "url",
estimatedCredits: 1,
});
for (const img of res.images) console.log(img.url);Mobile tip: prefer
responseFormat: "url". Base64 payloads can be megabytes and saturate the React Native bridge.
Image generation runs on its own daily credit bucket — exhausting chat tokens does not block image calls and vice versa. Only OpenAI-backed apps can call images.generate; Anthropic-backed apps return 400 unsupported_capability.
Image editing
Transform an existing image with a prompt — typical for portrait restyling, costume swaps, etc. Source image goes as base64 inside the JSON body (the platform's HTTP layer is JSON-only). On RN read the file with expo-file-system's readAsStringAsync(uri, { encoding: 'base64' }).
import * as FileSystem from "expo-file-system";
const base64 = await FileSystem.readAsStringAsync(playerPhotoUri, {
encoding: FileSystem.EncodingType.Base64,
});
const res = await platform.llm.images.edit({
model: "gpt-image-1",
prompt: "Render this person as a high-fantasy character portrait, watercolor style",
image: base64,
imageMediaType: "image/jpeg",
size: "1024x1024",
responseFormat: "url",
estimatedCredits: 1,
});
console.log(res.images[0].url);Body limit: Lambda HTTP API caps body at ~6 MB after base64 — keep raw image ≲ 4.5 MB. Downscale phone photos before encoding.
Optional mask (base64 RGBA PNG, transparent pixels = the area to repaint). Same provider rules as generate: OpenAI only, separate credit bucket.
API
createPlatformClient(opts) → PlatformClientplatform.config.get() → AppConfigResponseplatform.llm.chat(req) → LlmChatResponseplatform.llm.chatStream(req, signal?) → AsyncGenerator<LlmStreamChunk>platform.llm.object<T>(req) → LlmObjectResponse<T>platform.llm.images.generate(req) → ImagesGenerateResponseplatform.llm.images.edit(req) → ImagesEditResponseestimateImageTokens(image, provider, dimensions?) → numberfromZod(schema)from@amber-core/sdk-mobile/zod
The client injects Authorization: Bearer <token> and X-App-Id headers automatically. For non-streaming chat, object, images.generate, and images.edit, an X-Idempotency-Key is generated per call to make retries safe.
Migration from 0.1.0
LlmMessage now accepts content blocks alongside plain strings — old code ({ role: "user", content: "..." }) keeps working without changes. The streaming SSE delta event now carries multiple payload shapes discriminated by type ("delta" for text, "tool_use_*" for tool calls); existing code that reads chunk.content for chunk.type === "delta" is unchanged.
