NAME

Langertha::Skeid::Protocol::Ollama - Translate between the Ollama chat format and the upstream OpenAI call

VERSION

version 0.003

DESCRIPTION

Serves POST /api/chat, POST /api/generate and GET /api/tags. Ollama-specific field names — done_reason, prompt_eval_count, eval_count — live here and nowhere else in Skeid.

Ollama's messages are already OpenAI-shaped, so the request translation is small — but it is not empty: generation settings arrive nested under options with Ollama's own names.

Streaming is translated, not refused. stream: true on this route — and an absent stream field, which Ollama defaults to true — is rewritten to an OpenAI stream with stream_options.include_usage; the response is re-emitted as Ollama's newline-delimited JSON, one line per delta and a closing line that carries done, done_reason, and the token counts an Ollama client reads from. done_reason is the OpenAI finish_reason passed through verbatim — there is no translation to do. See Langertha::Skeid::Protocol::Ollama::Stream for the per-chunk rewrite and t/31-stream-translation.t for what the wire looks like end-to-end.

request_to_openai

my $openai_body = Langertha::Skeid::Protocol::Ollama->request_to_openai($body);

Turns an Ollama chat request into the OpenAI chat-completions body Skeid forwards. options.temperature and options.num_predict are lifted out of the nested hash to temperature and max_tokens; tools and tool_choice pass through unchanged. No other option is carried.

Tool-call history is made OpenAI-shaped: an assistant message's tool_calls get their arguments object encoded as a JSON string and an id (call_skeid_N) where they have none, and a tool message that names its call only by tool_name gets the tool_call_id of the matching unanswered call of the preceding assistant turn (the first unanswered one when no name matches).

format, Ollama's structured output, becomes response_format: "json" is {type => 'json_object'}, a JSON schema object is {type => 'json_schema', json_schema => {name => 'ollama_format', schema => ...}} with the schema as sent. No strict is set -- Ollama's format has no such switch. An empty string, null or any other value is no format, and no response_format is sent.

A user message's images, raw base64 strings, become an OpenAI content array: the message text as a text part, then one image_url part per image, each a data: URL whose media type is read from the image's magic bytes (PNG, JPEG, GIF, WebP; PNG otherwise, see "image_media_type" in Langertha::Skeid::Protocol). A message without images keeps its string content.

response_from_openai

my $ollama = Langertha::Skeid::Protocol::Ollama->response_from_openai($res);

Turns the upstream OpenAI response into an Ollama chat response. Tool calls come from Langertha::ToolCall, including Hermes-style calls recovered from plain text — when they are recovered, the text they were embedded in is stripped from the message content.

Token counts are reported under Ollama's names; done is always true because this path never streams.

generate_request_to_openai

my $openai_body = Langertha::Skeid::Protocol::Ollama->generate_request_to_openai($body);

Turns an Ollama /api/generate request into the same OpenAI chat-completions body "request_to_openai" builds for /api/chat, by way of a chat conversation: system, when given, becomes a system message, and prompt with its images becomes one user message -- so the images become image_url parts exactly as a chat message's do. model, options and format are read as on /api/chat.

Everything else a generate request can carry is not forwarded: think and options.seed (not carried on /api/chat either), and the fields that only mean something to an Ollama server's own prompt handling -- suffix, template, raw, context, keep_alive.

generate_response_from_openai

my $ollama = Langertha::Skeid::Protocol::Ollama->generate_response_from_openai($res);

Turns the upstream OpenAI response into an Ollama generate response: the answer text as response, done true, done_reason and the token counts under the same names as "response_from_openai". The text is passed as the model wrote it -- generate has no tool calls, so nothing is lifted out of it. Ollama's context (its token ids for the next call) and its timing durations are not reported; Skeid has neither.

tags_from_models

my $tags = Langertha::Skeid::Protocol::Ollama->tags_from_models($skeid->list_models(api_key_id => $id));

Renders a model list ("list_models" in Langertha::Skeid: hashes with model and engine) as an Ollama /api/tags answer, one entry per name, the same names /v1/models lists. family is the entry's engine, openaibase when it has none (an alias).

The fields Ollama clients expect but Skeid cannot know — size, digest, parameter size, quantisation — are filled with empty or 'unknown' placeholders rather than invented, so a client that displays them shows nothing instead of showing a lie.

manifest_endpoint

my $spec = Langertha::Skeid::Protocol::Ollama->manifest_endpoint;
# { dialect => 'ollama', path => '', capabilities => [ ... ] }

How this face appears in the provider manifest (skeid #29): ollama at the public root, and the capability flags "request_to_openai" actually carries to the upstream -- messages (a system message included), tools, options.temperature, options.num_predict (response size), stream, a message's images (image_input) and format as "json" or a schema (response_format_json_object, response_format_json_schema). A model is published here only with the capabilities declared for it that are in this list.

/api/generate needs no entry of its own: the ollama dialect names the whole Ollama API at this root, and the manifest's capabilities describe a chat call, which /api/chat is.

Not carried, so never claimed: options.seed and think. tool_choice is passed through when a client sends one, but the Ollama dialect has no such field, so no tool_choice_* flag is claimed.

error_body

my $body = Langertha::Skeid::Protocol::Ollama->error_body("Model 'x' is not available for this key");

Ollama's error envelope, { error => $message } -- the message as a plain string, not an object: the Ollama clients decode error as a string (the Go client's StatusError) and fail on anything else. Every error Skeid answers on /api/* is rendered from this, with the HTTP status of the failure, and so is the mid-stream error line (see "error_event" in Langertha::Skeid::Protocol::Ollama::Stream) (skeid #47).

SEE ALSO

Langertha::Skeid::Protocol::Ollama::Stream, Langertha::Skeid::Protocol, Langertha::Skeid::Proxy

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/langertha-skeid/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.