All notes

Routing and qualifying Ollama-backed Codex workers

My next round of Codex work added Ollama model discovery, provider-aware Desktop selection, a cloud-only root picker, and task-shaped worker qualification.

  • This is the fourth note in a series about mixed-provider Codex delegation.
  • Ollama model discovery needed a fallback for its OpenAI-compatible model-list shape.
  • Desktop model selection needed an opaque identity that preserved both provider and native model.
  • The root-task picker shows only cloud models because my hardware struggled with the initial context; smaller local models remain available to bounded workers.
  • A configured model only earns a worker role after it completes task-shaped tool, scope, and test checks.
On this page

In Making Codex subagents readable to Ollama providers, I gave third-party providers a plaintext child-task path. In Turning Ollama-backed Codex subagents into a usable worker pool, I added the lifecycle controls needed to manage those workers.

The remaining routing problems appeared at the application boundary. Codex had to find models that were not in its bundled catalog and keep provider identity intact when Desktop returned a selection.

Both paths had to preserve provider identity from the Desktop picker through to the actual model request.

Ollama model discovery uses a different shape

Readable tasks and lifecycle controls still do not help if Codex cannot discover the model.

Codex normally expects its richer model catalog. Ollama's OpenAI-compatible endpoint returns a simpler list:

JSON
{
  "object": "list",
  "data": [{ "id": "example-model:cloud", "object": "model" }]
}

I added a fallback parser for that shape. Provider-advertised IDs receive conservative model metadata instead of being treated as unknown OpenAI models. Each provider also gets its own model manager so a custom catalog does not pollute the OpenAI cache or inherit the wrong routing information.

This fixed model discovery, but it introduced another identity problem in Desktop.

A model name does not identify its provider

Desktop treats the model value returned by model/list as an opaque identifier. It later echoes that value when it creates a thread or starts a turn.

That becomes ambiguous when several providers can expose the same model slug. I needed the selection to preserve both pieces of information:

TEXT
provider id + native model id
              |
              v
versioned opaque selector
              |
              v
Desktop stores and returns it unchanged

The app server encodes the provider and native model into a versioned selector. When Desktop sends it back, Codex decodes it, confirms that the provider exists, and routes the native model name to that provider. Unknown providers and attempts to change the provider of an active thread are rejected.

The selector appeared in two places

The selector has to be normalized in more than one part of a Desktop request. Desktop can send the selected model as a top-level turn setting and inside collaborationMode.settings.model.

I initially decoded only the top-level value. The nested value then won and reached Halo as a literal model name beginning with v1.. Ollama correctly reported that no such model existed.

The regression test now follows the Desktop-shaped request. It obtains the selector from the model list, sends it in both fields, completes a turn, and checks that the provider received the native model name.

That test is intentionally more involved than a selector unit test. Encoding and decoding a value in isolation does not prove that Desktop, app server, thread configuration, and the model client agree on which value is public and which is provider-native.

Root tasks and bounded workers have different needs

I did not expose every Halo model in the Desktop task picker.

The initial context for a root Codex task can be much larger than the compact task packet sent to a bounded worker. A local model that performs well on a narrow implementation job may struggle before the first root turn begins.

On my hardware, local models struggled with that initial root-task context before useful work began. I therefore limited the Desktop picker to Halo cloud models. Smaller local models can still be assigned to named worker roles, where they receive compact task packets instead of the full session context.

This is a policy in my private build, not a general claim that cloud models always perform better. It reflects the limits of my hardware, the size of the initial context, and the reliability I observed in this specific setup.

Codex Desktop model picker showing OpenAI models followed by Halo models whose names are tagged cloud.

The root-task picker in my private Codex build. The Halo models shown here carry cloud in their tag. Smaller local models remain available to bounded worker roles.

Qualifying a model for a worker role

After routing worked, I still had to test whether each model could do the job.

I qualify worker candidates with disposable, task-shaped fixtures. A builder must use the available tools, change only the allowed files, run the relevant tests, and report the result accurately. Read-only workers have to respect their scope and return evidence the parent can check.

The checks separate several questions:

TEXT
Can the endpoint accept the request?
Can the model use the tool schema?
Can it complete the assigned task correctly?
Does it stay inside the allowed scope?
Is its latency acceptable for this role?

A raw generation response answers only the first part. I found models that could generate good text but failed before a Codex worker turn because the endpoint rejected a developer-role message. I found others that completed bounded tasks correctly but were too slow for interactive use.

The live matrix reflects those differences. Halo-backed models handle bounded implementation, small mechanical work, batch processing, and first-pass review. OpenAI-backed agents retain orchestration, architecture, security-sensitive review, release work, and final approval. The parent remains responsible for checking diffs, tests, and filesystem state before accepting offloaded work.

Current limits of the private fork

This is still a private, provider-specific system.

The worker provider is normally selected through a named role rather than an arbitrary provider argument on every spawn. Desktop selector generation contains Halo-specific policy. Transport is configured per provider instead of negotiated per model. Tool calling, developer messages, vision, structured input, and context limits still require separate qualification.

The private fork lets one Codex runtime coordinate a configured set of inference providers without giving those providers direct control of the local machine. It does not provide universal third-party agent support.

Across these four notes, the implementation progressed in this order:

TEXT
configure a provider
        |
        v
make child tasks readable
        |
        v
manage the worker pool
        |
        v
route and qualify the private system

I can now trace a Desktop selection to the native provider request and qualify workers against real task fixtures. I still keep the standard Codex build for application integrations that the private binary cannot use. A future note will describe that workflow.