83 Commits
Author SHA1 Message Date
arc53-machine c2d1893992 fix(admin): keyset export paging, rollup-free spend, consistent totals
Review pass over the preceding commits.

- The activity export paged by OFFSET. The journals are append-only and the
  feed is newest-first, so rows written mid-export shift the window down and
  repeat rows already emitted. Pages by keyset on (created_at, feed, id) now.
- top_token_users and the per-user breakdown counted run-level rollup rows.
  A scheduled run already has a row per LLM call, so its spend was billed
  twice -- invisible while the column was tokens, obvious once it was dollars.
  Both now exclude them, matching every other spend query.
- total_cost was summed from the per-model split, which drops rows with no
  model_id and so undercounted the figure printed above the series. Summed
  from the series instead.
- The CSV export encoded its detail cell without the fallback the NDJSON
  branch had, so a non-JSON-native value would have failed the stream.
- The activity view reset pagination in an effect, which fetched the stale
  page against the new filters before fetching again. Reset in the setters.
- The audit taxonomy is no longer re-exported from the helper module; the one
  caller that wanted it imports from where it lives.
2026-09-22 10:41:46 +01:00
Alex 43fad2a865 feat(quotas): quota_policies table and a per-call cost on token_usage
Migration 0033 adds quota_policies (instance default, team per-member
allowance, user override; a token budget and a USD budget per row) and
token_usage.cost. The column is added IF NOT EXISTS so a database that
already carries it upgrades cleanly.

Every usage row now records the call's USD cost from the model catalog;
bring-your-own models are recorded at $0.
2026-09-21 12:11:11 +01:00
arc53-machine f882ef49a7 refactor: read settings directly instead of getattr with a second default
About 85 call sites read a setting as getattr(settings, "NAME", fallback),
each carrying its own copy of the default. Every one of those names is a
field with a default on the model, so the fallback could never apply to
the real settings object; it only masked drift. Two had drifted:

- OPENAI_PROMPT_CACHE_KEY defaults to True on the model but the reader
  fell back to False, and two test stubs relied on that.
- SharePoint's MICROSOFT_AUTHORITY fallback to
  https://login.microsoftonline.com/<tenant> never fired, because the
  attribute always exists (as None), so MSAL got authority=None. The
  connector now derives the tenant authority when the setting is unset,
  as its test always assumed.

Four places read EMBEDDINGS_KEY straight from os.environ, skipping the
"None"/"" normalisation the model applies; they read the setting now.
Test stubs that replaced a module's settings with a SimpleNamespace list
every setting the code under test reads.
2026-09-17 11:14:34 +01:00
Alex 3030aba39d fix: handle non text uploads more carefully 2026-09-14 17:27:13 +01:00
Alex 108b8fd56b chore(deps): fastmcp 4 (mcp 2.2)
No code change needed: the server side (`FastMCP`, `@mcp.tool`,
`get_http_headers`, `http_app`) and the client side (`Client`, `BearerAuth`,
the three transports, and the `mcp.client.auth` / `mcp.shared.auth` OAuth
types imported directly) all kept their signatures.

Verified beyond the suite by driving the real `/mcp` mount over a live
uvicorn socket with the fastmcp client: tool discovery, a `search_docs` call
and bearer-token extraction all round-trip, with the token reaching
`search()` intact.

fastmcp 4 and mcp 2.2 also moved to httpx2, so the retry tuple's comment now
places them on that side of the split.
2026-09-12 17:54:18 +01:00
Alex f7baa1952f chore(deps): anthropic 1.5, openai 3.13, and the move to httpx2
Both SDKs' 1.x/3.x majors run on httpx2 (the maintained fork of httpx by its
original author, published by Pydantic at github.com/pydantic/httpx2, version
line 2.x) instead of httpx, which is what makes these two bumps one change.
httpx2 is now a declared dependency because two modules import it directly.
Everything else in the 0.x -> 1.x / 2.x -> 3.x change lists is absent here:
no Text Completions, no `with_raw_response`, no raw `output_format` dicts, no
Bedrock client, and `requires-python` is already 3.12.

Three code changes, all forced by the bump:

The BYOM DNS-pinning client in `docsgpt/security/safe_url.py` is handed to
`OpenAI(http_client=...)`, and the SDK rejects an old-httpx client at
construction — which would have taken the SSRF guard offline. It is built on
httpx2 now, and its `sni_hostname` extension carries a `str` rather than
ascii bytes: httpcore passes the value straight to
`ssl.SSLContext.wrap_socket`, and the truststore backend httpx2 uses for the
default system trust store encodes it instead of accepting bytes. Bytes
therefore failed every real handshake while passing the existing tests, which
stub the transport out; the test now pins the type and says why.

The stream-retry error tuple in `docsgpt/llm/base.py` named `httpx`
exceptions only. The two libraries' exception classes are unrelated types, so
after the bump the retry silently stopped firing for openai and anthropic
while still working for google-genai and elevenlabs. It now covers both
stacks, with a parametrized test over each.

anthropic 1.x dropped temperature/top_p/top_k from `messages.create`'s
signature (passing one raises TypeError) without dropping them from the API,
so the provider forwards them through `extra_body`. The wire request is
unchanged and a model that rejects them 400s exactly as before.
2026-09-12 17:44:18 +01:00
Alex 574f96341e refactor: rename the application package to docsgpt
The backend import package is now docsgpt, the name it will carry on PyPI;
application was far too generic to install into anyone's site-packages.
git mv plus a mechanical rewrite of every import, dotted string and path
reference: 734 Python files, the compose files, Dockerfile, workflows, docs,
setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage
config, .gitignore. Behaviour is unchanged.

Kept for one release:
- A top-level application package whose meta-path finder resolves
  application.x.y to the already-imported docsgpt.x.y object, so old imports
  and entry points (celery -A application.app.celery,
  uvicorn application.asgi:asgi_app) keep working with a FutureWarning.
- Celery registers every application.* task name as an alias of its
  docsgpt.* task on start-up, so messages queued by the previous release still
  run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries
  the previous release wrote are left unread instead of firing twice.

The backend image builds from the repository root (docker build -f
docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore
allow-lists docsgpt/ and application/ and keeps caches, local data, .env
files, the sample index files and the Dockerfile out. Compose and the image
workflows point at the new context.
2026-09-07 10:20:43 +01:00
Alex 427d85d737 fix(llm): commit the staged head hash unconditionally on a recorded response
An unchained request with no system message staged None, and the record
step skipped the commit, so the previous head hash survived a transcript
that never received a head; a later chained request restoring that head
would have omitted it. The staged value is now committed as-is, None
included. Also pins each rejection predicate of is_usable_compression_point
with its own test.
2026-09-05 11:26:25 +01:00
Alex 5837c300df fix(llm): record Responses chain state only for completed responses
The non-streaming path recorded the response id, reasoning items and the
staged system-head hash before checking the terminal status, so a
response that came back failed could become the id the next call chains
onto and commit a head the stored transcript never received. Chain state
is now recorded after the status checks; a response cut off by
max_output_tokens still records it, since its input was accepted.
2026-09-05 11:07:07 +01:00
Alex dbee30a048 fix(compression,llm): keep the summary across mid-execution compression, ignore empty saved points, commit the head hash on success
Three review findings on the bounded-chain change.

Mid-execution compression rebuilt the conversation from the in-flight
messages, which after a turn-start reuse hold only the recent turns: the
summary living in the system prompt never reached the compressor, so the
new summary replaced the old one, and the persisted point's query_index
was relative to that shortened list. The summary the agent is running
under now rides into the synthetic conversation as its latest point
(query_index -1, so every in-flight query is new), for both the database
and the in-memory path, and the database path persists the index of the
saved conversation's last row.

Saved points with an empty summary, which earlier versions wrote, were
treated as reusable: get_compressed_context sliced the raw history away
and the effective token count made the conversation look small. Point
selection everywhere now takes the latest usable point (non-blank
summary, positive token count) and falls back to the raw history when
there is none.

The chained system-head hash was committed while building the request,
so a transport failure followed by the same-primary retry omitted a
changed system message. The hash is now staged per request and committed
only when the provider records the response.
2026-09-05 10:39:58 +01:00
Alex 1f86139b9c fix(compression,llm): address review — exact point dedupe, marked summary rows, opaque cache key
- append_compression_point only skips a point when both query_index and
  compressed_summary are present and match the last one; points without
  those fields (as in the repository tests) were all being treated as
  duplicates.
- The incremental compression tail and the orchestrator's "anything new
  since the last point" check exclude the visible summary row, which the
  prompt already receives through existing_compressions.
- Summary rows carry a persisted metadata marker; replay filters on the
  marker, and falls back to the label only for rows written before it that
  have no tool calls and no per-turn metadata, so a user who types the
  label text keeps their turn.
- The prompt_cache_key is a hash of the user id, never the id itself.
- Describe truncation="auto" as dropping the oldest items.
2026-09-05 09:30:13 +01:00
Alex 04d358ab69 fix(llm,compression): bound cross-turn Responses chaining and make compression stick
In store mode every user turn chained onto the previous response, so the
provider's stored transcript grew without bound (measured: 889k prompt
tokens for a 37k-token saved history) while every local guard, the
compression pipeline included, measured the saved history. Each chained
tool round also re-sent the system message, which the server appends rather
than dedupes, and a saved compression point was applied exactly once, in the
turn that made it.

Chaining is now bounded. A turn starts from the saved history when the
previous turn's reported prompt reached the chain budget (default: the
model's context window), when the conversation was compressed after that
turn was produced, or when OPENAI_RESPONSES_CHAIN_ACROSS_TURNS is off.
Chained rounds omit an unchanged system head (hash carried in the persisted
Responses state). truncation="auto" is available behind a setting as a
backstop against a chain that outgrows the model's window.

Compression: a saved point is applied at every turn start; the threshold
counts the summary plus the queries after the point instead of the raw
history; re-compression summarises only the tail on top of the last point;
the mid-execution path marks itself persisted and resets the provider chain
so the rebuilt messages are the context; an empty summary is rejected; the
visible "[Context Compression Summary]" rows are no longer replayed as
history; appending the same point twice is a no-op.

Cache hints: a per-user prompt_cache_key and an optional
prompt_cache_retention on Responses API calls.

Measured on Azure with the same client shape as production (stateless
OpenAI client, server-side tools, PDF part): tokens billed on the sixth turn
fell from 58k to 35k, tool rounds add tens of tokens instead of ~2.8k, the
turn after a compression reused the saved summary in under two seconds
instead of re-summarising, and the round after a mid-execution compression
started from the compressed context instead of the full stored transcript.
2026-09-05 00:28:51 +01:00
Alex 9db11f18f5 fix(attachments,usage): validate content behind a BOM, judge by the live parser table
Review follow-ups.

A BOM told the sniff which encoding to read, but was also taken as the
verdict: three prepended bytes let any binary through, including as
notes.txt. A BOM now only selects the test — UTF-8 falls through to the
byte rules on the remainder, UTF-16/32 decode and judge the characters
(NUL, unprintable, or replacement chars from bytes the decoder could not
read). Real Notepad-Unicode text still passes, mp4-behind-a-BOM does not,
in either language.

The gate treated the full parser table as a given, but without docling the
fallback extractor has no .tif/.tiff/.bmp/.webp/.vtt/.xml handler, so those
suffixes skipped the content check and reached the plain-text fallthrough —
the original bug, one install away. The worker now passes the keys of the
extractor it actually built, making the second gate stricter than the
route's static one rather than a copy of it.

Cache bins: extraction coerced a missing value to 0 and only non-zero bins
were recorded, so a provider reporting cached_tokens=0 persisted as NULL —
indistinguishable from "not reported", and OpenAI reports exactly that on
every uncached request. Bins are now carried as Optional and recorded when
not None, which is what the nullable columns and the NULL-means-unknown
comment already assumed. Anthropic's cache_read/cache_creation bins had the
same shape and are fixed alongside; the int-or-None coercion is shared in
llm/base.py.
2026-09-03 09:46:55 +01:00
Alex 6d0319fb63 feat: better cached token usage logging 2026-09-03 09:01:28 +01:00
Alex 3f88dc7b57 fix: better snapshot and gate 2026-08-22 14:00:15 +01:00
Alex e96ff8658c fix: more stability for durable tasks, retry strategy, refactor dead
code
2026-08-22 09:44:09 +01:00
Alex e6fb46560c fix: minor graph rag improvements 2026-08-20 13:33:06 +01:00
Alex cf6b2cc1c4 feat: remove trialing system msg 2026-08-20 11:38:58 +01:00
Alex 0b36257202 feat: parser improvements and fixes 2026-08-19 23:49:26 +01:00
Alex a70362a5f3 feat: better fallback shim 2026-08-14 17:30:36 +01:00
Alex 0a15ce8fbb feat: attachment provenance 2026-08-13 14:30:28 +01:00
Alex 2565906f88 chore(deps): bump 47 backend dependencies
Bumps the Python backend dependency set, holding back the ones that are
resolver-blocked or that regress behaviour this repo depends on.

Notable upgrades:
  cryptography 46.0.7 -> 50.0.0 (requires msal 1.37.0, which relaxes its cap)
  protobuf 6.33.6 -> 7.35.1 (floats opentelemetry-* to 1.44.0)
  openai 2.32.0 -> 2.53.0, anthropic 0.88.0 -> 0.121.0
  google-genai 1.73.1 -> 2.17.0 (the 2.0 break is scoped to the Interactions
    API, which this repo does not use)
  fastmcp 3.2.4 -> 3.4.6, gunicorn 25.3.0 -> 26.0.0
  starlette 1.0.0 -> 1.6.0, uvicorn 0.42.0 -> 0.52.1
  sentence-transformers 5.3.0 -> 5.7.0, numpy 2.4.4 -> 2.5.1
  faiss-cpu 1.13.2 -> 1.15.0, pillow -> 12.3.0 (pinned; security release)
  pypdf 6.9.2 -> 6.15.0, lxml 6.0.2 -> 6.1.1 (CVE-2026-41066)

Code changes needed by the bumps:
  - openai >= 2.53 rejects a falsy api_key at construction. Keyless
    OpenAI-compatible backends (Ollama, llama.cpp, vLLM) legitimately have
    none, and pydantic-settings yields "" for a bare `API_KEY=` in .env, so
    both call sites now fall back to a placeholder.
  - Flask >= 3.1.2 tears a stream_with_context request down twice, and a
    ContextVar token may only be reset once. The log-context teardown hook
    is now idempotent.
  - sentence-transformers renamed get_sentence_embedding_dimension to
    get_embedding_dimension in 5.4; use the new name with a fallback.

Held back deliberately:
  torch 2.11.0      - 2.13.0 drags torchvision 0.26 -> 0.28 and the whole
                      docling stack; torch is only probed for cuda.empty_cache()
  tokenizers 0.22.2 - transformers pins <=0.23.0 and no stable 0.23.0 exists
  transformers      - capped <5.9.0: 5.9+ breaks docling's PDF layout model on
                      Apple Silicon (MPS float64), matching docling-core's own
                      darwin pin
  docling 2.84.0    - 2.118.1 needs rapidocr >=3.9.1 and conflicts with
                      transformers on macOS; wants its own PR with a
                      golden-corpus diff
  redis 7.4.0       - 8.x defaults socket_timeout to 5s, silently capping the
                      blocking reads in the device broker and SSE tail
  websockets 16.0   - google-genai caps <17.0
  marshmallow       - dataclasses-json hard-caps <4; spec tightened to match
  langchain block   - langchain-community 0.4.2 deletes the Qdrant vectorstore
                      this repo imports; langchain 1.3.x needs websockets <16
2026-08-09 12:30:44 +01:00
Alex 795e39a6bc fix: source authorization, silent retrieval failures, and prompt structure
Source access control
---------------------
`active_docs` is client-supplied and reached the retriever unchecked, and the
retriever queries `WHERE source_id = <id>` with no owner predicate — so any
caller could pass any source id to /stream or /api/answer and have another
tenant's documents quoted back, while /api/sources/<id>/search correctly
refused the same id. Gate it through `can_access`, the helper the guarded
endpoints already use, and filter `self.source` down to the authorized set.
Fails closed: no principal, or a check that errors, drops the source.

Three sibling paths had the same gap:

- workflow agent nodes: `AgentNodeConfig.sources` is written verbatim from
  client JSON at save time and nothing validated it, so a node could name any
  tenant's source. Gate against the workflow owner, so shared workflows keep
  reading their owner's sources like shared agents do.
- /api/share: `_resolve_source_pg_id` resolved any id with no ownership
  predicate and baked it into the agent the share creates; /api/search then
  searched it. Authorize before attaching.
- search_service: re-resolve the ids stored on an agent row instead of
  trusting them, so a row written by any future path with the same gap cannot
  be read back.

Team grantees previously lost their source's retrieval config: the post-check
read was still owner-scoped, so it missed and fell back to defaults (an
`agentic_tool` source was bulk-prefetched for every grantee). Read unscoped
after `can_access` passes.

Retrieval
---------
`PGVectorStore._ensure_table_exists` created an IVFFlat index on the empty
table it had just created. IVFFlat computes centroids at build time, so those
centroids were random, and combined with the `source_id` post-filter a source
with hundreds of embedded chunks returned zero rows — retrieval reported no
documents, the model answered from memory, and nothing was logged. Stop
creating the index (exact search is correct and fast well past the sizes most
deployments reach); raise `ivfflat.probes` to sqrt(lists) where an index still
exists; and re-run a short indexed search exactly, since post-filtering means
no index setting can guarantee a full result. `graphrag` had the same
empty-table index with no fallback at all.

Also: bound `chunks` to 0-500 on both the request and agent paths (0 still
means "skip retrieval"), let a source's configured `retrieval.chunks` outrank
the request body, and cap ClassicRAG's per-source floor at
max(top_k, n_sources) so attaching sources cannot inflate the result set.

Silent failures
---------------
An empty retrieval was invisible to both the model and the client: the `source`
event was suppressed when the list was empty, so "searched and found nothing"
looked identical to "no source attached", and the prompt said nothing at all.
Emit the event always, and tell the model when a search ran and returned
nothing. A file that parses to nothing now fails ingest with a message naming
the cause instead of storing an embedding of the empty string. `score_threshold`
returns warnings when the active store or retriever cannot honour it.

Prompt structure
----------------
Retrieved documents move from the system prompt into the user turn, with the
injection guard restated next to them: they change every turn (defeating prefix
caching), they are third-party text that should not carry system authority, and
routing them through the query budget makes them truncatable rather than
silently crowding it out. Documents are shed lowest-ranked-first before the
question is touched.

The six chat presets (3 tones x 2 retrieval modes) differed only in their
Answering section; they are now composed from single-source fragments at load
time, not through Jinja inheritance, which would have opened a file-read
surface in the template sandbox and broken the tool-prefetch parser. Per-tool
guidance moves out of the prompt into tool schemas, so it travels with the tool
and cannot render when the tool is absent. A plain-text custom prompt is staged
as a persona value inside the skeleton instead of replacing it wholesale — it
used to silently lose the injection guard, platform block, memory and
attachments, and its braces are now inert.

Other fixes
-----------
- agents/base: an oversized system prompt drove the query budget negative and
  dispatched a full-price request with an empty question; raise instead.
- llm/anthropic: migrate off the retired Text Completions API. It flattened
  history to first+last message and ignored tools entirely. Adds the missing
  Anthropic handler, without which every tool call was silently dropped.
- sources/upload: `sitemap` had no branch, so every sitemap ingest died on a
  TypeError; `validate_url` now rejects a falsy URL cleanly.
- workflow nodes: retrieved documents never reached the node agent, so a
  classic node with a source and an ordinary prompt answered "I have no
  documents" while the run reported completed.
- parser/bulk: copy the metadata dict, or every chunk reports the last chunk's
  token_count.
- crawler_loader: carry the page title, or citations render the whole chunk
  body as the label.
2026-08-08 10:21:52 +01:00
Alex a51dd10cc7 fix(openai): stop telling users their readable PDF could not be processed
_upload_file_to_openai short-circuited on `"openai_file_id" in attachment`.
Attachment dicts are built by row_to_dict from `SELECT *`, so that key is
always present and is NULL until an upload caches one — every fresh
attachment read as a cache hit. The method returned None without
uploading and, critically, without raising, so the except branch that
inlines the extracted text never ran. The part went out as
`{"file_id": None}`, which _resolve_file_part degraded to
`[File 'upload.pdf' could not be processed]`.

The model then relayed that to the user while thousands of tokens of
extracted text sat unused on the attachment row. Reproduced live against
the Foundry deployment: the model answers "I couldn't access the attached
PDF because it could not be processed; please re-upload it or paste the
text". The same bug was fixed in google_ai.py by 8e9f661e ("Truthy check,
not membership") which never touched this file; d29a7aaf later turned the
resulting hard 400 into this silent wrong answer.

Truthy check restored, a falsy file_id now raises so the fallback runs,
and the fallback tests `content` truthily — it too was a membership test,
so an empty extraction sent a bare "File content:" header that reads as a
successfully-read document. With no content at all we emit a note naming
the user's own file instead of the `upload.pdf` placeholder.

The file part deliberately carries `file_id` only: sending `filename`
alongside it is rejected with
`400 Unknown parameter: 'messages[0].content[1].file.filename'`.

Two consequences of making this path live for the first time are handled
here. `attachments.openai_file_id` is a single global column, so ids are
now stamped with an endpoint fingerprint — an id minted against one
deployment replayed against another answers "No such File object" on
every retry, permanently, since the row keeps the bad id. A foreign or
legacy unscoped value is treated as a miss and re-uploaded, so it
self-heals. And inlined text is bounded against the model's context
window, because _enforce_context_window runs before attachments are
merged into the messages and nothing downstream would trim a 200-page
extraction.
2026-08-05 11:25:28 +01:00
Alex 32597df153 fix: cleanup models, better retry strat 2026-07-23 10:22:10 +01:00
Alex b4211944f1 fix: dedup nit 2026-07-22 15:16:27 +01:00
Alex d29a7aaf19 fix: bug on openai compat file upload 2026-07-22 14:28:52 +01:00
Alex e4265ce38a fix: mini issues 2026-07-20 20:35:18 +01:00
Alex 3cbd284948 fix: multitool call openai issue 2026-07-20 18:29:24 +01:00
Alex 2b8afb8c10 fix: cleanup comments 2026-07-16 09:45:18 +01:00
Alex 1e4e2ff24a fix: better v1 compat and keepalive on long thinking 2026-07-16 09:45:04 +01:00
Alex ca5f80995d fix: accurate per-call token usage and oversized-context guards
Token accounting:
- Drain each tool round's provider stream to exhaustion before running
  tools and recursing, so the usage decorator persists exactly one
  token_usage row per LLM call, at call end. Previously every round's
  generator was abandoned mid-iteration and flushed together at request
  teardown, writing N near-identical rows stamped with the final
  round's provider counts (duplicate billing).
- Consume the Chat Completions include_usage terminal chunk (it arrives
  after finish_reason and was never read) so streamed calls record
  provider-exact token counts instead of tiktoken estimates.
- Claim provider-reported usage once per call (_last_usage_claimed) so
  a late-finalized generator can never adopt another call's counts.

Oversized-context guards:
- Enforce Responses API function_call/function_call_output pairing in
  the input builder (drop unpaired items; bypassed for store-mode
  previous_response_id chaining where calls are matched server-side).
- Hard pre-send context gate: shrink oversized tool results and refuse
  payloads that cannot fit the model's window before dispatch, so a
  hopeless request is never sent or billed.
- Cap a single tool result entering the LLM context
  (TOOL_RESULT_MAX_TOKENS, default 20000); the tool journal and
  persistence keep the full result. Applied on the resume/continuation
  path too.
- Skip the fallback attempt when the payload cannot fit the fallback
  model's context window (10% estimation slack).
- Compression: never save a compression point that does not reduce
  tokens; bound oversized verbatim fields kept after a compression
  point (COMPRESSION_RECENT_FIELD_MAX_TOKENS, default 8000).

Robustness fixes from review:
- Google parallel function calls: complete index-less ToolCalls are no
  longer merged into one another (dict arguments raised TypeError on
  +=; second call could execute with the first call's arguments).
- Trailing-frame failures after a delivered answer no longer error the
  stream or restream the whole answer from the fallback
  (_stream_reached_finish).
- In-memory compression falls back to minimal pruning when the summary
  is not smaller than the original.
- keep<=0 guard in the middle-truncation helpers (a tiny cap returned
  marker + full text).

Frontend: tooltip on the Analytics tokens stat card explaining that
agent tool loops re-send conversation context on every step.
2026-07-15 21:20:39 +01:00
Alex f33bdaa52d fix: minor usage tracking improvements 2026-07-13 09:38:17 +01:00
Alex 0adf88abed feat: improved compatability layer and responses api handing 2026-07-13 09:37:58 +01:00
Alex 9f19bc64b5 fix: minor parser and id hardening 2026-07-09 18:43:48 +01:00
Alex 75e07af78f fix: minor issue fixes 2026-06-30 18:36:06 +02:00
Alex 9da7d732ce Keep tool-call message history well-formed when a tool errors
When an executed tool call raises, append the assistant message carrying the
tool_calls before the tool error result, mirroring the success path. Previously
only the tool result was appended, leaving a tool message with no preceding
tool_calls parent, which made the next provider completion reject the request.
Every tool_call id in a batch is now answered even when some calls fail.
2026-06-24 22:58:07 +01:00
Alex 9a69e89096 fix: assistant input content must be output_text, not input_text
Live /v1/responses rejects input_text for the assistant role
(input[N].content[0] -> "Supported values are: 'output_text' and
'refusal'"). Revert the previous review-driven change: encode assistant
turns as output_text and user/system turns as input_text. The SDK type
union suggested input_* only, but the API enforces output_text per-role —
confirmed against the live endpoint.
2026-06-03 15:00:14 +01:00
Alex 27f9ddc8e0 fix: address PR review — Responses input encoding + finish Azure removal
- Replay prior-turn assistant text as input_text, not output_text: a
  Responses easy-input message only accepts input_* content parts, so the
  old shape would 400 on the second turn of every conversation (default
  store=false resends prior turns inline).
- Always request include=["reasoning.encrypted_content"] so in-turn
  reasoning carryover works whether or not the response is also stored
  server-side (OPENAI_RESPONSES_STORE).
- Add detail="auto" to input_image parts.
- Remove the now-dead azure_openai option from setup.sh / setup.ps1 and
  the docs, completing the AzureOpenAILLM removal.
- Tests: correct the input_text / image-detail / include assertions and
  add parallel tool-call and stream-error coverage.
2026-06-03 14:56:28 +01:00
Alex 142a9201f4 feat: add OpenAI Responses API support via per-model api_flavor
Route opted-in models to /v1/responses while keeping Chat Completions as
the default. The flavor is selected per-model in the catalog YAML and
threaded through ModelCapabilities (with a new reasoning_effort field).

- Translate chat-shaped messages/tools/structured-output into the
  Responses input / text.format / flattened-tool shapes at the provider
  boundary; stored history and intra-turn tool round-trips need no
  migration.
- Normalize the typed SSE event stream back into the existing handler
  contract (content / thought / tool-call chunks) so the streaming
  accumulator and OpenAI handler stay unchanged.
- Carry encrypted reasoning items across the in-turn tool loop
  (provider-local, keyed by call_id) for stateless reasoning continuity.
- OPENAI_RESPONSES_STORE opt-in enables server-side state and
  previous_response_id chaining across turns, persisted in message
  metadata.
- Set gpt-5.5 to api_flavor: responses, reasoning_effort: medium.
- Remove AzureOpenAILLM and the azure_openai provider/catalog.
2026-06-03 13:43:04 +01:00
Alex a5f735575f fix: backup model response parsing 2026-06-01 11:30:37 +02:00
Alex badd3830b7 feat: log gen end 2026-05-31 18:00:51 +02:00
Alex 85a2811e44 feat: log gen starts 2026-05-31 17:42:42 +02:00
Alex 7bb54e0f10 Fix: change model retry strategy 2026-05-31 16:43:40 +02:00
Alex b2d1d7c2c3 Deepseek (#2499)
* feat: add deepseek

* feat: deepseek reasoning tool fix
2026-05-25 23:49:25 +01:00
Alex b4c4ab68f0 feat: durability and idempotency keys (#2450)
* feat: durability and idempotency keys

* feat: more durable frontend

* fix: tests

* fix: mini issues

* fix: better json validation

* fix: tests
2026-05-04 23:25:41 +01:00
Alex f0c39dec23 feat: more logs on stream finish 2026-04-28 02:27:02 +01:00
Alex 552bfe016a fix: better token counting and fixes cache 2026-04-28 01:47:53 +01:00
Alex 8e9f661efc fix: attachments 2026-04-28 00:38:27 +01:00
Alex 82c71be819 feat: better logging 2026-04-28 00:14:43 +01:00