Commit Graph

48 Commits

Author SHA1 Message Date
ollie b4c88a33d3 route: fetch real models from backends, pick by number
Route() now queries all configured backends for their model lists,
presents them numbered to the classifier, and parses the number.
No more hardcoded OLLIE_ROUTE_MODEL_FAST/POWER env vars.
2026-07-18 17:09:55 +02:00
ollie 8a995c1b13 route: use numbered classification for reliability
Ask classifier to reply with 1 or 2 instead of full model names.
Previous approach produced garbled partial names.
2026-07-18 17:05:48 +02:00
ollie 1b92c49c27 backend: add Generate() and Route() stateless one-shot functions
Moves /generate and /route logic from 9p server to core so both
9p and dbus frontends can use them.
2026-07-18 16:59:29 +02:00
ollie 3a6be7a8a9 kiro: use q.<region>.amazonaws.com for ListModels (same as kiro-cli)
The management.*.kiro.dev endpoint was slower/less reliable. Switch to
the same q.<region>.amazonaws.com endpoint that kiro-cli uses, deriving
the region from the profile ARN or runtime endpoint.
2026-07-18 12:54:38 +02:00
ollie 93f672df20 kiro backend: cache Models() for 24h, cache sqlite tokens for 60s
Models() now caches at the backend level (shared across sessions) with
a 24h TTL, eliminating repeated expensive HTTP calls to AWS.

The sqliteKiroAuth.load() now caches token reads for 60s, avoiding
shelling out to sqlite3 on every API call (previously 3x per Models
fetch).
2026-07-18 01:40:19 +02:00
ollie a50149d075 fix: send profileArn for social accounts, improve error messages
- Remove IsSocial guard from sqliteKiroAuth.ProfileARN(); the Kiro API
  now requires profileArn on all requests including personal accounts
- Add OLLIE_KIRO_PROFILE_ARN env var for static token auth
- Add repeated error detection (3x identical error triggers corrective nudge)
- Include expected format in tool dispatch error messages
2026-07-11 16:08:44 +02:00
Levi Neely 75f137dda5 backend/kiro: port fixes from goq amzcodewhisp
- Switch endpoint from q.*.amazonaws.com to runtime.*.kiro.dev
- Use management.*.kiro.dev for ListModels
- Fix content encoding: plain string instead of json.RawMessage
- Fix tool results: always use text field (upstream rejects bare JSON primitives)
- Normalize tool call arguments (deduplicate keys)
- Expand retry conditions: add 502, 503, 408, InsufficientModelCapacity
- Add SQLite busy-timeout and Go-level retry for lock contention
- Add 30s refresh timeout and fullLoad before refresh
- Add SSO cache fallback for token refresh
- Add jitter to token expiry checks
- Add sqliteHasNewUsableAccessToken for cross-process recovery
- Add envState to current message
- Update user-agent strings to 1.3.15/2.5.1
- Set X-Amzn-Codewhisperer-Optout to true
- Add reasoning/thinking support (additionalModelRequestFields)
- Add counter-thinking tags on user messages before assistants
- Handle reasoningContentEvent in stream
2026-07-10 11:43:18 +02:00
Levi Neely 0fc9c9613c kiro: update API version and retry transient 500s
The Kiro/CodeWhisperer API started rejecting tool-use requests from
older API versions (0.1.14474) with InternalServerException. Update
to 0.1.16551 and appVersion 0.12.333 to match kiro-cli.

Also add retry with linear backoff (1-5s) for transient 500 errors
which still occur intermittently even with the correct version.
2026-07-10 10:28:48 +02:00
ollie bc7a536ced fix: round-trip reasoning_content for DeepSeek thinking models
DeepSeek requires reasoning_content to be echoed back on assistant
messages. Store reasoning from stream events on Message and serialize
it as reasoning_content in the OpenAI wire format.
2026-05-28 19:04:03 +02:00
Levi Neely 2b914ce150 codewhisperer: image content block support in user messages and tool results 2026-05-25 23:14:40 +02:00
ollie 4b4dab3f02 backend: add tests for image ContentBlock encoding (Anthropic + OpenAI)
TestBuildAnthropicMessages_UserImageBlocks: user message with ContentBlocks
TestBuildAnthropicMessages_ToolImageBlocks: tool result with image blocks
TestBuildAnthropicMessages_UserPlainTextFallback: nil ContentBlocks uses Content
TestEncodeMessages_UserWithImageContentBlocks: OpenAI image_url encoding
TestEncodeMessages_UserWithImageContentBlocks_JSON: wire format verification
2026-05-25 18:06:01 +02:00
ollie adb686bc1d backend: add ContentBlock/ImageSource types and multi-modal image encoding for Anthropic and OpenAI
Add canonical ContentBlock and ImageSource types to backend.go.
When Message.ContentBlocks is set, it takes precedence over Content.

Anthropic: new anthropicImageSource type, anthropicContentBlocks() helper
converts canonical blocks to wire format. buildAnthropicMessages handles
ContentBlocks on user and tool messages.

OpenAI: new openAIImageURL type, openAIContentBlocks() helper converts
canonical blocks to wire format (image_url with data: URIs).
encodeOpenAIMessages uses ContentBlocks when present.
2026-05-25 17:08:18 +02:00
Levi Neely 5985ab7d20 backend/kiro: exponential backoff on ThrottlingException
Retry up to 5 times with exponential backoff (5s, 10s, 20s, 40s, 80s)
when the Kiro backend returns ThrottlingException or HTTP 429.
Previously these errors were sent as a fatal StopReason, causing the
agent loop to abort immediately.
2026-05-23 14:45:29 +02:00
Levi Neely 1485decdbb backend: fix copilot API paths
Copilot API has no /v1 prefix. Add pathPrefix field to OpenAIBackend
(defaults to /v1) and apply extraHeaders in fetchModels. Copilot sets
pathPrefix to / so it hits /models and /chat/completions directly.
2026-05-21 19:43:23 +02:00
Levi Neely 09afd071ed strip quotes from env vars 2026-05-21 19:12:56 +02:00
Levi Neely 8960cd9754 fix TestBackends: expect gemini in backend list 2026-05-21 19:07:39 +02:00
Levi Neely 2ef2dd4290 backend: add gemini backend (Google AI Studio / OpenAI-compat) 2026-05-21 17:04:52 +00:00
Levi Neely 3867d52689 add generation params: top_p, top_k, min_p, top_a, repetition_penalty, max_completion_tokens, response_format, reasoning_effort, include_reasoning, stop, verbosity 2026-05-16 23:54:04 +02:00
Levi Neely c812f0882e freeloader: fix 404 tool_unsupported detection and FIFO-drain recovery
- 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>
2026-04-28 18:47:34 +02:00
Levi Neely 78e5416550 core: freeloader infrastructure — turnError hook, ToolUnsupportedError, prompt.prev, MemoryStore
- 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>
2026-04-28 17:17:25 +02:00
Levi Neely 9f1d7a5ec8 core: caching risk-reduction tests
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>
2026-04-28 14:33:29 +02:00
Levi Neely 905bf4e9a9 core: tests and emission fixes for parallel tool execution and caching
- 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>
2026-04-28 13:13:32 +02:00
Levi Neely efac665ffe core: tool schema caching and large tool result caching
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>
2026-04-28 12:22:45 +02:00
Levi Neely ba14c8f07f core: cache system prompt on OpenRouter+Claude via content block encoding
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>
2026-04-28 12:14:46 +02:00
Levi Neely cf6fee36e2 core: Anthropic prompt caching and cache-aware cost accounting
- 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>
2026-04-28 12:01:14 +02:00
Levi Neely 370171bf80 agent: context overflow recovery and OLLIE_MAX_SESSION_COST spending cap
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>
2026-04-27 18:35:39 +02:00
Levi Neely 59ee25b010 agent: per-turn and session cost tracking in USD
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>
2026-04-27 18:14:12 +02:00
Levi Neely 625257dea1 agent: retry on transient backend errors and mid-stream drops
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>
2026-04-27 17:54:27 +02:00
Levi Neely eabe17a1b1 backend: stream extended thinking / reasoning tokens
- Add StreamEvent.Reasoning for incremental thinking deltas
- Add GenerationParams.ThinkingBudget to enable Anthropic extended thinking
- Anthropic: parse thinking_delta events; set Anthropic-Beta header and
  thinking request param when ThinkingBudget > 0
- OpenAI: parse delta.reasoning (o-series) and delta.reasoning_content
  (OpenRouter) fields
- Loop: emit "reasoning" events wrapped in <think>/</think> markers
- FormatEvent: append reasoning content directly to the chat log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 19:01:27 +02:00
Levi Neely c7ad2ca68a backend: require OLLIE_BACKEND/OLLIE_MODEL; no silent ollama default
- 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>
2026-04-26 18:25:39 +02:00
Levi Neely b54c5c08f9 kiro: implement social token refresh via desktop.kiro.dev 2026-04-26 18:11:21 +02:00
Levi Neely c6d6f2f162 backend: recover DeepSeek DSML tool calls leaking into content stream
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>
2026-04-26 13:17:29 +02:00
Levi Neely d89f458473 backend: mark codewhisperer as intentionally untested, remove test 2026-04-21 22:09:27 +02:00
Levi Neely 2264f3f11d agent: fix backend race, split commands.go, use OLLIE_TMP_PATH
- Add backend.NewWithName(name) to avoid os.Setenv race on OLLIE_BACKEND
- Extract handleCommand into commands.go (core.go 1264 -> 859 lines)
- Replace hardcoded /tmp/ollie with OLLIE_TMP_PATH (guaranteed by env.Ensure)
2026-04-21 21:44:39 +02:00
Levi Neely d957bda8a0 backend: remove unreachable NewRequestWithContext guards (URL validated in ctor) 2026-04-21 15:47:34 +02:00
Levi Neely 5cfa5a0df9 backend: 100% coverage for buildAnthropicMessages and streamAnthropicSSE
- 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
2026-04-21 15:45:46 +02:00
Levi Neely 38dd34ea20 backend: cover Ollama Models/ContextLength HTTP paths, constructor URL validation 2026-04-21 15:42:27 +02:00
Levi Neely 13b17cd777 backend: extract parseOllamaContextLength, 100% coverage 2026-04-21 15:39:52 +02:00
Levi Neely 6a60f5014b backend: extract newFromEnv, 100% coverage for new.go 2026-04-21 15:36:26 +02:00
Levi Neely 9d8398bab0 backend: test coverage for new.go (loadEnvFile, openAIName, Backends) 2026-04-21 15:34:01 +02:00
Levi Neely 21827c57eb backend: extract stream parsers, shared HTTP, url.URL, comprehensive tests
- Extract streamOpenAISSE, streamOllamaNDJSON from ChatStream goroutines
  into standalone functions taking io.Reader for direct testability
- Remove ctx parameter from streamAnthropicSSE (caller handles cancellation)
- Extract streamRequest into backend.go: shared HTTP execution, 429/error
  handling, and goroutine lifecycle for all three backends
- Extract encodeOpenAIMessages, encodeOpenAITools from ChatStream
- Replace string baseURL with *url.URL; validate in constructors
- Constructors (NewOpenAI, NewOllama, NewAnthropic, NewCopilot) now return error
- Remove unreachable json.Marshal and http.NewRequestWithContext error branches

ChatStream coverage: 100% (OpenAI, Ollama, Anthropic)
Stream parsers: 100% (OpenAI, Ollama), 96.6% (Anthropic)
streamRequest: 100%

Test files:
- openai_encode_test.go: message/tool encoding, JSON round-trip
- openai_stream_test.go: SSE parsing via io.Reader
- ollama_stream_test.go: NDJSON parsing via io.Reader
- anthropic_stream_test.go: SSE parsing, mapAnthropicStopReason
- openai_test.go: HTTP integration, error paths, models, context length
- backend_test.go: contract checks (all interface methods), stream text/usage/
  tool calls/tool conversation, HTTP errors, rate limits for all backends
2026-04-21 15:32:03 +02:00
Levi Neely 46272f22a6 test: sandbox coverage + backend contract tests
- 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>
2026-04-21 13:00:06 +02:00
Levi Neely 92034b5342 add OLLIE_CFG_PATH and OLLIE_DATA_PATH env vars
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.
2026-04-17 15:31:19 +02:00
Levi Neely a035913514 fix: only use json wire field for JSON objects in tool results
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.
2026-04-13 16:21:45 +02:00
Levi Neely aa32d17e81 backend: add Backends() listing supported backend names 2026-04-13 14:45:14 +02:00
Levi Neely 4786866e66 fix: strip inline comments in env file parser, fix backtick syntax errors
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.
2026-04-13 09:32:56 +02:00
Levi Neely 45a094f46e backend: add ContextLength, Models, and auto-compaction
- 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.
2026-04-10 19:35:44 +02:00
Levi Neely 97b85e5163 refactor: promote backend/config to pkg/, extract TUI to ollie-tui
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>
2026-04-09 22:05:35 +02:00