- backend: add HTTP 404 and "no endpoints found" to isToolUnsupported
so OpenRouter 404s are correctly classified as tool_unsupported
- core: drain FIFO on error return from executeTurn so freeloader's
resubmitted prompt (queued via fifo.in while running) is picked up
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: ToolUnsupportedError type, isToolUnsupported detector (400/422)
- backend: classifyError maps error types to hook payload strings
- agent: HookTurnError constant; TurnError func on agentConfig set at init
- loop: fire TurnError hook on first backend error; if ran, skip retries
- store: MemoryStore — in-memory KV Store implementation
- store: Session.prevPrompt field; prompt.prev read-only session file
- hooks: inject payload map as OLLIE_* env vars in runHookCmd
- agent: agentSpawn payload includes model field
- agent: limitretry state emitted on RateLimitError backoff
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
loop_prune_test.go — three new prune tests:
- ReadWriteReadCycle: pre-write read pruned, post-write read kept (primary invariant)
- AssistantMessagesUnaffected: only role:"tool" messages are dropped, never assistant messages
- NonExecuteCodeUnaffected: non-execute_code tool calls don't confuse the path tracker
loop_cache_test.go — one new cache test:
- IdenticalParallelReads: two goroutines simultaneously miss the cache, both execute,
results are consistent and no panic (sync.Map race safety)
cost_test.go — six computeCostUSD unit tests:
- BaseTokens, ClaudeCacheReadDiscount (10%), NonClaudeCacheReadDiscount (50%),
CacheCreationSurcharge (125%), AllFieldsCombined, UnknownModelZero
openai_encode_test.go — four OpenRouter cache injection tests:
- OpenRouterClaude_SystemCacheControl: system content encoded as array with cache_control
- OpenRouterClaude_LastToolCacheControl: last tool gets cache_control:ephemeral
- OpenRouterNonClaude_NoCacheControl: non-Claude model on openrouter gets no injection
- OpenAI_NoCacheControl: plain openai backend never injects cache_control
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- loop_parallel_test.go: four tests covering concurrent execution (barrier
pattern), serial tool breaking a batch, cancellation filling all results,
and nil ClassifyTool falling back to serial
- loop.go: emit "tool" events for all cancelled tool calls (early-ctx-err
path in execOne, top-level ctx check, and post-batch fill) so every
dispatched tool call has a visible result regardless of cancellation timing
- anthropic_stream_test.go: five tests covering tool schema cache_control
placement (last tool only, single-tool edge case) and tool result caching
(large gets cache, small does not, last of multiple large results wins)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tool schema caching:
- Mark the last tool with cache_control:ephemeral before each Anthropic
request; Anthropic caches all preceding tools as a prefix
- Same for OpenRouter+Claude via openAITool.CacheControl extension
Large tool result caching (Anthropic only):
- In buildAnthropicMessages, scan backwards through the encoded output
and mark the last tool_result block ≥4096 bytes with cache_control
- 4096 bytes ≈ 1024 tokens, Anthropic's minimum cache entry size
- OpenAI wire format has no block structure for tool results; skip
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When OpenRouter routes to a Claude model, encode the system message as
a content-block array with cache_control:ephemeral instead of a plain
string. OpenRouter passes this through to Anthropic, enabling the same
90% cache-read discount as the native Anthropic backend.
Detection: b.name=="openrouter" && model contains "claude-".
Encoding: MarshalJSON on openAIMessage outputs content as array when
ContentBlocks is set, preserving the existing *string path for all
other cases with no test changes required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Anthropic system prompt sent as a cacheable content block
(cache_control: ephemeral) so repeated turns reuse the cached prefix
- Parse cache_read_input_tokens and cache_creation_input_tokens from
Anthropic message_start; parse cached_tokens from OpenAI
prompt_tokens_details
- Normalise Usage so CachedInputTokens is always excluded from
InputTokens regardless of provider
- computeCostUSD now takes backend.Usage and applies correct cache
discounts: 10% read / 125% creation for Anthropic, 50% read for OpenAI
- Session tracks TotalCachedInputTokens / TotalCacheCreationTokens;
/usage shows cached token count when non-zero
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On ContextOverflowError (400 with context-length body), restore pre-turn
snapshot, compact, and retry the turn once. Covers runaway context growth
that slips past the proactive 75% threshold.
OLLIE_MAX_SESSION_COST (USD) blocks turn start and interrupts mid-turn
via the usage event handler, which fires between every tool round.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Usage events now carry a 4th field (costUSD) so OpenRouter's reported
cost flows through unchanged. For backends that only report token counts
(Anthropic, direct OpenAI, Ollama), cost is computed from a prefix-keyed
pricing table. The higher-fidelity source always wins.
Per turn:
- turnInputTokens/turnOutputTokens/turnCostUSD reset before each turn
- recordTurnCost() stores LastTurnCostUSD, adds to SessionCostUSD
- "costLast=$X.XXXX" emitted as an info event at turn end
Session file "cost" returns costLast/costSession key=value pairs.
Agent interface gains Cost() string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only HTTP 429 triggered a retry. Network failures, 5xx
responses, and mid-stream drops all hard-failed the turn.
- Add TransientError for 5xx and network-level failures; streamRequest
wraps client.Do errors and >=500 responses in it
- Merge pre-stream and mid-stream retry into one loop (maxTransientRetries=3)
covering rate limits, transient errors, and pure stream drops
- On stream drop with ctx cancelled: record partial state and return
(existing pause behaviour preserved)
- On stream drop without cancellation: close any open <think> block,
emit retry countdown, restart ChatStream from the same history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NewWithName and newFromEnv now error when OLLIE_BACKEND is unset
instead of falling back to ollama silently
- createSession reads OLLIE_MODEL as fallback when model= is not given
- Update test to assert error on missing OLLIE_BACKEND
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DeepSeek's native tool-call format uses <|DSML|invoke> tokens. When
OpenRouter fails to convert them to standard tool_calls chunks, they
arrive as raw content text. Detect the marker, buffer the content, and
parse it into ToolCall structs at [DONE] if no structured tool calls
accumulated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Test all buildAnthropicMessages branches: system concat, assistant
tool-calls-only, nil arguments default, tool batching, unknown role,
full multi-turn conversation
- Test streamAnthropicSSE done re-entry: events after message_stop and
error are ignored
- Remove unreachable done guard in process closure
- sandbox: remove dead code (DefaultConfig, Load/Save, LayeredConfig, Merge,
Version, SystemDefaults, validateName, ConfigPath, OpenSandbox); unexport
internal symbols; LoadSandbox now takes io.Reader; loadSandboxConfig owns
file opening; 83% package coverage via new sandbox_test.go
- backend: add BaseURL field to AnthropicBackend for testability; new
backend_test.go runs checkContract against all five implementations
(Ollama, OpenAI, Copilot, Anthropic, CodeWhisperer) via httptest servers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all hard-coded ~/.config/ollie and ~/.local/share/ollie paths
with a new paths package that reads OLLIE_CFG_PATH and OLLIE_DATA_PATH,
falling back to the previous defaults when unset.
Bare JSON scalars (0, true, null, "string") and arrays pass
json.Valid() but the Kiro API rejects them in the json field with
ValidationException. Once such a tool result is in session history,
every subsequent request fails, making the session unrecoverable.
Restrict the json path to content starting with '{' so only actual
JSON objects are sent that way; everything else uses the text field.
loadEnvFile now strips " # ..." suffixes from values so inline comments
in ~/.config/ollie/env don't leak into environment variables. Also add
kiro to the supported-backends error message and fix two raw-string
backtick escaping errors in tool descriptions.
- Add ContextLength(ctx) int to Backend interface; each backend fetches
from its API (OpenAI /v1/models, Ollama /api/show, CodeWhisperer
ListAvailableModels) with caching. Falls back to 128k default.
- Add Models(ctx) []string to Backend interface for model discovery.
- Add /models command to list available models.
- Auto-compact before each turn when estimated tokens >= 75% of context length.
Move internal/backend and internal/config to pkg/ so external consumers
can import them. Remove cmd/ollie and internal/tui — they now live in
the separate ollie-tui repo which imports ollie as a library.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>