Advanced recipes
Complete @orcushq/gemini examples for multi-step tool calling, conversation history, streaming, structured output, images, batch processing, and error handling.
These examples combine the features covered elsewhere in the Gemini section. Each example initializes an authenticated GeminiWeb client. Configure session cookies and an XSRF token from the same signed-in Gemini Web session before running the examples.
Multi-step tool calling
Tool calling is prompt based. Your application executes each requested tool, appends an assistant message carrying the returned tool_calls and one tool message per result, then invokes again until the model stops requesting tools. Always cap the number of rounds.
import 'dotenv/config'import { GeminiWeb, type GeminiMessage } from '@orcushq/gemini' const cookies = process.env.GEMINI_COOKIESconst xsrfToken = process.env.GEMINI_XSRF_TOKEN if (!cookies?.trim() || !xsrfToken?.trim()) { throw new Error( 'GEMINI_COOKIES and GEMINI_XSRF_TOKEN are required', )} const model = new GeminiWeb({ model: 'gemini-3.6-flash', cookies, xsrfToken,}) const tools = [ { name: 'get_weather', description: 'Get the current weather for a city.', parameters: { type: 'object', properties: { city: { type: 'string' }, }, required: ['city'], }, },] async function executeTool( name: string, args: Record<string, unknown>,): Promise<unknown> { switch (name) { case 'get_weather': { const city = typeof args.city === 'string' ? args.city : undefined if (!city?.trim()) { throw new Error('Invalid get_weather arguments') } // Replace this fixture with your application implementation. return { city, temperature: 32, condition: 'Sunny', } } default: throw new Error('Unknown tool: ' + name) }} async function run(prompt: string): Promise<string> { const messages: GeminiMessage[] = [ { role: 'user', content: prompt }, ] const MAX_ROUNDS = 8 for (let round = 0; round < MAX_ROUNDS; round++) { const response = await model.invoke(messages, { tools }) if (response.toolCalls.length === 0) { return response.content } messages.push({ role: 'assistant', content: response.content, tool_calls: response.toolCalls, }) for (const call of response.toolCalls) { let result: unknown try { result = await executeTool( call.name, call.arguments, ) } catch (error) { result = { error: error instanceof Error ? error.message : 'Tool failed', } } messages.push({ role: 'tool', name: call.name, content: JSON.stringify(result), }) } } throw new Error('Maximum tool-call rounds exceeded')} console.log(await run('What is the weather in Indore?'))Keep tools bound on every call
Pass `{ tools }` on each `invoke()`. The definitions keep the tool catalogue in the prompt and enable tool-call parsing on the next response. The supported handoff uses the tool name and string content; there is no `tool_call_id` field to set.
Managing conversation history
The SDK does not store conversation state. Keep the history in your application and resend the relevant messages each call. Trim or summarize old turns to control prompt growth, since the whole array is flattened into one prompt.
import 'dotenv/config'import { GeminiWeb, type GeminiMessage } from '@orcushq/gemini' const cookies = process.env.GEMINI_COOKIESconst xsrfToken = process.env.GEMINI_XSRF_TOKEN if (!cookies?.trim() || !xsrfToken?.trim()) { throw new Error( 'GEMINI_COOKIES and GEMINI_XSRF_TOKEN are required', )} const model = new GeminiWeb({ model: 'gemini-3.6-flash', cookies, xsrfToken,}) const system: GeminiMessage = { role: 'system', content: 'Answer in one short paragraph. Never invent facts.',} const history: GeminiMessage[] = [system] async function ask(question: string): Promise<string> { history.push({ role: 'user', content: question }) const MAX_TURNS = 10 const trimmed = history.length <= MAX_TURNS ? history : [system, ...history.slice(-(MAX_TURNS - 1))] const response = await model.invoke(trimmed) history.push({ role: 'assistant', content: response.content, }) return response.content} console.log(await ask('My name is Alex.'))console.log(await ask('What is my name?'))Session IDs are not memory
`response.metadata.sessionId` can be reused through the `sessionId` option, but it does not load earlier messages. Continue sending history yourself.
Streaming responses
stream() yields text deltas as they arrive. Process each chunk incrementally and inspect the final chunk when the stream completes.
import 'dotenv/config'import { GeminiWeb } from '@orcushq/gemini' const cookies = process.env.GEMINI_COOKIESconst xsrfToken = process.env.GEMINI_XSRF_TOKEN if (!cookies?.trim() || !xsrfToken?.trim()) { throw new Error( 'GEMINI_COOKIES and GEMINI_XSRF_TOKEN are required', )} const model = new GeminiWeb({ model: 'gemini-3.6-flash', cookies, xsrfToken,}) const stream = await model.stream( 'Write a short essay about software testing.',) for await (const chunk of stream) { if (chunk.content) { process.stdout.write(chunk.content) } if (chunk.done) { console.log('\nFinished') console.log('Requested model:', chunk.requestedModel) console.log('Served model:', chunk.model) console.log('Model fallback:', chunk.modelFellBack) console.log('Fallback reason:', chunk.fallbackReason) }}Structured output with validation
Define a Zod schema and use withStructuredOutput() to generate schema-guided instructions and validate the reply. Validation failures raise GeminiStructuredOutputError, so handle them explicitly when the result drives application logic.
import 'dotenv/config'import { z } from 'zod'import { GeminiWeb, GeminiStructuredOutputError,} from '@orcushq/gemini' const cookies = process.env.GEMINI_COOKIESconst xsrfToken = process.env.GEMINI_XSRF_TOKEN if (!cookies?.trim() || !xsrfToken?.trim()) { throw new Error( 'GEMINI_COOKIES and GEMINI_XSRF_TOKEN are required', )} const model = new GeminiWeb({ model: 'gemini-3.6-flash', cookies, xsrfToken,}) const Ticket = z.object({ title: z.string(), priority: z.enum(['low', 'medium', 'high']), tags: z.array(z.string()),}) const structured = model.withStructuredOutput(Ticket, { instructions: 'Use the exact field names from the schema.',}) try { const result = await structured.invoke( 'Create a bug report for a login redirect loop that blocks checkout.', ) console.log(result.value.priority) console.log(result.value.tags)} catch (error) { if (error instanceof GeminiStructuredOutputError) { console.error( 'Extraction failed:', error.message, error.details, ) } else { throw error }}For schema shapes that cannot be represented by the generated JSON Schema, pass an explicit jsonSchema in the options rather than relying on automatic conversion.
Image inputs with validation
Images can be supplied as HTTPS URLs, data URLs, base64 strings, or raw bytes. Validate image URLs and sizes before sending them when they come from untrusted input.
import 'dotenv/config'import { GeminiWeb, resolveImageSource,} from '@orcushq/gemini' const cookies = process.env.GEMINI_COOKIESconst xsrfToken = process.env.GEMINI_XSRF_TOKEN if (!cookies?.trim() || !xsrfToken?.trim()) { throw new Error( 'GEMINI_COOKIES and GEMINI_XSRF_TOKEN are required', )} const model = new GeminiWeb({ model: 'gemini-3.6-flash', cookies, xsrfToken,}) async function describe(url: string): Promise<string> { if (!url.startsWith('https://')) { throw new Error('Only HTTPS image URLs are allowed') } const image = await resolveImageSource(url, { maxBytes: 5_000_000, }) const response = await model.invoke([ { role: 'user', content: [ { type: 'text', text: 'Describe this image in one sentence.', }, { type: 'image', data: image.bytes, mime_type: image.mimeType, }, ], }, ]) return response.content} console.log( await describe('https://example.com/photo.png'),)Structured output with batch
Use structured.batch() to process multiple inputs through the schema. Results preserve input order. If any invocation or schema validation fails, inspect the resulting error and any available partial results.
import 'dotenv/config'import { z } from 'zod'import { GeminiWeb } from '@orcushq/gemini' const cookies = process.env.GEMINI_COOKIESconst xsrfToken = process.env.GEMINI_XSRF_TOKEN if (!cookies?.trim() || !xsrfToken?.trim()) { throw new Error( 'GEMINI_COOKIES and GEMINI_XSRF_TOKEN are required', )} const model = new GeminiWeb({ model: 'gemini-3.6-flash', cookies, xsrfToken,}) const Person = z.object({ name: z.string(), age: z.number(),}) const structured = model.withStructuredOutput(Person) try { const results = await structured.batch( [ 'Return a person named Vikas aged 21.', 'Return a person named Priya aged 34.', ], { concurrency: 2 }, ) for (const result of results) { console.log(result.value) }} catch (error) { if ( error instanceof Error && error.name === 'BatchAggregateError' ) { const aggregate = error as Error & { results?: readonly unknown[] errors?: readonly unknown[] } console.error('Some inputs failed:', aggregate.errors) console.log('Available results:', aggregate.results) } else { throw error }}Structured batch outcomes
`withStructuredOutput()` provides `invoke()`, `stream()`, and `batch()`. If you need a fulfilled or rejected outcome for every input, use `batchSettled()` on the client and validate the returned content separately where necessary.
Typed error handling
Use SDK error classes to handle authentication, rate limits, cancellation, and other known failure categories. Keep retries bounded and avoid exposing session credentials in logs.
import 'dotenv/config'import { GeminiWeb, GeminiAbortError, GeminiRateLimitError, GeminiAuthenticationError, isGeminiError,} from '@orcushq/gemini' const cookies = process.env.GEMINI_COOKIESconst xsrfToken = process.env.GEMINI_XSRF_TOKEN if (!cookies?.trim() || !xsrfToken?.trim()) { throw new Error( 'GEMINI_COOKIES and GEMINI_XSRF_TOKEN are required', )} const model = new GeminiWeb({ model: 'gemini-3.6-flash', cookies, xsrfToken,}) async function safeInvoke( prompt: string,): Promise<string | undefined> { try { const response = await model.invoke(prompt) return response.content } catch (error) { if (error instanceof GeminiAbortError) { return undefined } if (error instanceof GeminiRateLimitError) { throw new Error( 'Gemini rate limit reached. Respect retryAfterMs before retrying.', { cause: error }, ) } if (error instanceof GeminiAuthenticationError) { throw new Error( 'Check the Gemini session credentials.', { cause: error }, ) } if (isGeminiError(error)) { throw new Error( 'Gemini ' + error.code + ': ' + error.message, { cause: error }, ) } throw error }} console.log(await safeInvoke('Explain API reliability briefly.'))