Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
743c6e9
fix(api): treat message audio/function_call null-empty as omit; fail-…
seonghobae Aug 16, 2026
e3a6e0e
fix(api): treat message weight 0/1/null as omit-equivalent; fail-clos…
seonghobae Aug 16, 2026
4a95b9a
fix(api): fail-closed on unknown chat message fields and legacy funct…
seonghobae Aug 16, 2026
82038d3
fix(api): treat message prefix null/false as omit; fail-closed on true
seonghobae Aug 16, 2026
1a196b0
fix(api): treat chat max_tool_calls null/empty as omit; fail-closed o…
seonghobae Aug 16, 2026
3a0d35e
fix(api): treat Completions max_tool_calls null/empty as omit; fail-c…
seonghobae Aug 16, 2026
9d10fa9
fix(api): treat stream_options null flags as omit-equivalent no-ops
seonghobae Aug 16, 2026
b108251
fix(api): fail-closed unknown stream_options keys and tools-path mess…
cursoragent Aug 16, 2026
856adb8
fix(api): fail-closed stream, model, and sampling on tools passthrough
cursoragent Aug 16, 2026
1a575db
fix(api): fail-closed stream, model, and sampling on tools passthrough
cursoragent Aug 16, 2026
9328d11
fix(api): fail-closed attribution and routing on tools passthrough
cursoragent Aug 16, 2026
169a382
fix(api): fail-closed empty messages and spend keys on tools passthrough
cursoragent Aug 16, 2026
be0abae
fix(api): fail-closed non-string user content on tools passthrough
cursoragent Aug 16, 2026
f6dfae5
docs(api): tell buyers non-string user content fails closed on tools
cursoragent Aug 16, 2026
b950724
fix(api): fail-closed batch routing hints on tools passthrough
cursoragent Aug 16, 2026
dbfa0e5
docs: tell buyers to omit batch routing on tool-calling requests
cursoragent Aug 16, 2026
c5a816f
fix(api): fail-closed seed, stop, n, and logprobs on tools passthrough
cursoragent Aug 16, 2026
7a07562
fix(api): fail-closed empty messages before tools passthrough
cursoragent Aug 16, 2026
ffad9cb
fix(api): fail-closed seed/stop/n/logprobs on tools passthrough
cursoragent Aug 16, 2026
18e6263
fix(api): apply request temperature on streamed route completions
cursoragent Aug 16, 2026
bc73dd2
fix(api): pop null tool.function description/parameters/strict before…
cursoragent Aug 16, 2026
7e9a339
docs: record passthrough knob and stream temperature fixes in CHANGELOG
cursoragent Aug 16, 2026
3406210
feat(api): SSE-proxy tools and response_format on chat completions
cursoragent Aug 16, 2026
6372c25
merge: bring #601 omit-real and stream temperature onto SSE tip
cursoragent Aug 16, 2026
0717921
fix(api): fail-closed tool_calls keys and trace/mode on tools path
cursoragent Aug 16, 2026
8d7d534
fix(api): emit mock tool_calls deltas for streamed function tools
cursoragent Aug 16, 2026
a50032e
fix(api): apply top_p and penalties on streamed route completions
cursoragent Aug 16, 2026
baf2e84
docs: cite Holtzman nucleus sampling for streamed route knobs
cursoragent Aug 16, 2026
1550324
fix(api): treat padded tool_choice none as omit on mock tools
cursoragent Aug 16, 2026
2356202
fix(api): bind bare invoice numbers on mock lookup_balance
cursoragent Aug 16, 2026
644cc49
fix(api): synthesize mock content after bound tool results
cursoragent Aug 16, 2026
d074598
test(api): assert bare invoice bind on HTTP JSON and SSE
cursoragent Aug 16, 2026
e234c4a
fix(api): bind invoice no/nr/inv# on mock lookup_balance
cursoragent Aug 16, 2026
31b6bc1
test(api): reject forged tool_call_id beside a real assistant call
cursoragent Aug 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Changelog

All notable changes to this project are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- SSE-proxy `tools` and `response_format` on `/v1/chat/completions` when
`stream=true`. Function-tool mocks emit `delta.tool_calls` and finish as
`tool_calls` (invoice `lookup_balance` binds `INV-9` from the prompt);
content-only `response_format` streams still match the JSON body; live
providers are piped verbatim. Next action: send `stream=true` when the
client reads SSE; include the invoice id in the user text; omit
`stream_options.include_usage`.

### Fixed

- Synthesize a final `content` / `stop` answer (JSON and SSE) when the
buyer POSTs a `role=tool` observation bound to a prior assistant
`tool_call_id`. Mock `lookup_balance` no longer emits a second
`tool_calls` hop. The observed invoice id, balance, and status appear
in the answer. Empty or whitespace `tool_choice` with `tools` is
omit-equivalent `none` (the empty string is written back as `none`
before the provider hop). Next action: after the first tool call,
POST the tool output with the same `tool_call_id` and read `content`.
Send `none` when you want no first-hop tool call; omitted
`tool_choice` stays auto and still emits `lookup_balance`.
- Fail closed on unknown assistant `tool_calls` entry and `function` keys
(`unknown_tool_call_fields` / `unknown_tool_call_function_fields`) on both
the orchestration path and the tools / `response_format` SSE proxy.
Optional `index` is a non-negative integer or JSON `null`. Next action:
send only `id`, `type`, `function`, and optional `index` on assistant
`tool_calls`.
- Fail closed on tools / `response_format` for non-boolean
`include_orchestration_trace` and unknown `mode` before the JSON or SSE
proxy. Next action: send a boolean or omit the trace flag; send
`auto` / `route` / `conduct` or omit `mode`.
- Treat official-SDK JSON `null` on optional `tools[].function.description`,
`parameters`, and `strict` as omit-real: the keys are popped before
`proxy_completion` so upstream providers see an omitted field, not a null
schema. Non-null wrong types still fail closed with named `invalid_tools`.
Next action: send those fields only when you have a real string, JSON Schema
object, or boolean; SDK defaults of `null` are safe.
- Fail closed on tools passthrough for `seed`, `stop`, `n>1`, `logprobs`,
`logit_bias`, and out-of-range penalties — the same named errors as the
orchestration path. Next action: omit those knobs on tool-calling requests.
- Apply the request `temperature` on streamed route completions instead of
silently using `0.2`. Next action: send the temperature you want; streaming
no longer changes the sampling policy.
- Apply request `top_p`, `presence_penalty`, and `frequency_penalty` on
streamed route completions the same way `chat()` already does. Next
action: send the nucleus and penalty knobs you want; streaming no longer
drops them.
- Treat whitespace-padded `tool_choice` (`" none "`, `"\\tnone\\n"`) as
exact `none` on mock JSON and SSE tools: keep `content` / `stop` instead
of emitting `tool_calls`. The validator now writes the stripped token
back so live providers see `none`. Next action: send `none` to skip
tools; incidental padding is still omit-equivalent.
- Bind bare invoice numbers in mock `lookup_balance` (`invoice 4419` →
`INV-4419`) instead of defaulting to `INV-9` when the buyer omits the
`INV-` prefix. Clerk aliases (`invoice no. 4419`, `invoice nr 4419`,
`invoice is 4419`, `inv#4419`) bind the same digits. Next action: put
the invoice number in the user text; prefixed, bare, and clerk-alias
forms all bind.

### References

- OpenAI. (2024). *Create chat completion*. OpenAI API reference.
https://platform.openai.com/docs/api-reference/chat/create
- Bray, T. (Ed.). (2017). *The JavaScript Object Notation (JSON) data
interchange format* (RFC 8259). Internet Engineering Task Force.
https://doi.org/10.17487/RFC8259
- OpenAI. (2024). *Streaming API responses*. OpenAI API documentation.
https://platform.openai.com/docs/guides/streaming-responses
- WHATWG. (n.d.). *Server-sent events*. HTML Living Standard.
https://html.spec.whatwg.org/multipage/server-sent-events.html
- OpenAI. (2024). *Function calling*. OpenAI API documentation.
https://platform.openai.com/docs/guides/function-calling
- Schick, T., Dwivedi-Yu, J., Dessì, R., Raileanu, R., Lomeli, M.,
Hambro, E., Zettlemoyer, L., Cancedda, N., & Scialom, T. (2023).
Toolformer: Language models can teach themselves to use tools.
*Advances in Neural Information Processing Systems, 36*.
https://arxiv.org/abs/2302.04761
- Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., &
Cao, Y. (2023). ReAct: Synergizing reasoning and acting in language
models. *International Conference on Learning Representations*.
https://arxiv.org/abs/2210.03629
- CEN. (2017). *Electronic invoicing — Part 1: Semantic data model of
the core elements of an electronic invoice* (EN 16931-1:2017).
European Committee for Standardization.
https://standards.cencenelec.eu/ (BT-1 Invoice number; cite + link
only — CEN texts are not OA.)
- Holtzman, A., Buys, J., Du, L., Forbes, M., & Choi, Y. (2020). The
curious case of neural text degeneration. *International Conference on
Learning Representations*. https://arxiv.org/abs/1904.09751
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Non-mock providers must use `https://` URLs and a **resolvable KV credential**

One public interface:

- `/v1/chat/completions` accepts normal chat messages, and `"stream": true` returns an OpenAI-compatible `text/event-stream` of `chat.completion.chunk` deltas terminated by `data: [DONE]`. In **route** mode the worker's tokens are streamed live as they arrive from the provider (real token streaming); in **conduct** mode the multi-step answer is produced then framed as deltas (a workflow can't honestly token-stream a synthesizer that hasn't run yet).
- `/v1/chat/completions` accepts normal chat messages, and `"stream": true` returns an OpenAI-compatible `text/event-stream` of `chat.completion.chunk` deltas terminated by `data: [DONE]`. In **route** mode the worker's tokens are streamed live as they arrive from the provider (real token streaming); in **conduct** mode the multi-step answer is produced then framed as deltas (a workflow can't honestly token-stream a synthesizer that hasn't run yet). Tool-calling and `response_format` requests SSE-proxy a single pool agent so SDK `stream=true` clients receive chunks (including `delta.tool_calls`) instead of JSON. Offline `mock://` function tools emit the same `tool_calls` deltas (invoice `lookup_balance` binds `INV-9`). `/v1/completions`, `/v1/responses`, and `/v1/embeddings` are the matching compatibility surfaces. Send `stream_options.include_usage` / `include_obfuscation` as omitted, `null`, or `false`; `true` and unknown keys fail closed. On tool-calling requests send a non-empty `messages` array of objects; omit `routing.channel=batch` and `latency_tolerant=true` — those hints fail closed instead of billing a silent sync completion. Also omit `seed`, `stop`, `n>1`, and `logprobs`; this gateway does not apply them. Streamed route requests apply the `temperature`, `top_p`, `presence_penalty`, and `frequency_penalty` you send; they do not silently fall back to `0.2` or drop nucleus/penalty knobs. Optional `tools[].function.description`, `parameters`, and `strict` sent as JSON `null` are popped before the provider hop so SDK defaults stay omit-equivalent. Assistant `tool_calls` history may include only `id`, `type`, `function`, and optional `index` — unknown keys fail closed. Non-boolean `include_orchestration_trace` and unknown `mode` also fail closed on tool-calling requests.
- `TaskOrchestrator.complete()` decides whether to route to one worker or run a short workflow.
- `TaskOrchestrator.compare_to_baseline(prompts, mode)` (CLI `--eval PROMPT...`) measures the orchestration engine against a single-worker baseline — per-prompt and aggregate latency plus a structural coverage delta (contributing steps + verifier-pass presence). It is a measured tradeoff report, not a human-quality claim.
- Responses include orchestration mode metadata, and trusted callers can request the full trace for audit.
Expand Down
39 changes: 29 additions & 10 deletions contextual_orchestrator/cost_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ class UsageRecord:

def as_dict(self) -> Dict[str, Any]:
"""Flatten the record (attribution inlined) for JSON + SQL storage."""
# Execution identity is evidence of what ran — never a client-chosen tag.
# Account/service/team/group/company remain descriptive attribution.
row = {
"usage_record_id": self.usage_record_id,
"created_at": self.created_at,
Expand Down Expand Up @@ -583,12 +585,12 @@ def _seed_dimension_catalog(self) -> None:
ph = self._placeholder()
cur = self._conn.cursor()
for order, (name, label, _column) in enumerate(ATTRIBUTION_DIMENSION_CATALOG):
cur.execute(
cur.execute( # nosemgrep -- sqlalchemy-execute-raw-query FP: only the DB-API placeholder char is interpolated; the value is bound.
f"SELECT 1 FROM cost_attribution_dimensions WHERE dimension_name = {ph}", # nosec B608 - ph is a DB-API placeholder.
(name,),
)
if cur.fetchone() is None:
cur.execute(
cur.execute( # nosemgrep -- sqlalchemy-execute-raw-query FP: only DB-API placeholder chars are interpolated; values are bound.
"INSERT INTO cost_attribution_dimensions "
f"(dimension_name, dimension_label, dimension_order) VALUES ({ph}, {ph}, {ph})", # nosec B608 - ph is a DB-API placeholder.
(name, label, order),
Expand All @@ -602,7 +604,7 @@ def append(self, record: UsageRecord) -> None:
placeholders = ", ".join(ph for _ in _USAGE_COLUMNS)
columns = ", ".join(_USAGE_COLUMNS)
cur = self._conn.cursor()
cur.execute(
cur.execute( # nosemgrep -- sqlalchemy-execute-raw-query FP: columns are the fixed _USAGE_COLUMNS constant; values are bound.
f"INSERT INTO llm_usage_records ({columns}) VALUES ({placeholders})", # nosec B608 - columns are fixed _USAGE_COLUMNS.
tuple(row.get(column) for column in _USAGE_COLUMNS),
)
Expand All @@ -622,7 +624,7 @@ def query(self, start: Optional[int] = None, end: Optional[int] = None) -> List[
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
columns = ", ".join(_USAGE_COLUMNS)
cur = self._conn.cursor()
cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # nosec B608 - columns and clauses are fixed.
cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # nosec B608 - columns and clauses are fixed. # nosemgrep -- sqlalchemy-execute-raw-query FP: fixed columns and clause templates; all values are bound.
return [dict(zip(_USAGE_COLUMNS, values)) for values in cur.fetchall()]


Expand Down Expand Up @@ -681,14 +683,31 @@ def record_usage(
) -> UsageRecord:
"""Compute cost, build a :class:`UsageRecord`, persist it, and return it."""
if isinstance(attribution, dict) or attribution is None:
dims = AttributionDimensions.from_mapping(attribution)
# Strip caller-controlled execution identity before mapping so a
# client cannot spoof model/provider rollups (buyer-bill honesty).
if isinstance(attribution, dict):
cleaned = {
key: value
for key, value in attribution.items()
if key not in {"model_name", "provider", "upstream_api"}
}
else:
cleaned = None
dims = AttributionDimensions.from_mapping(cleaned)
else:
dims = attribution
# Keep the model_name dimension aligned with the served model unless the
# caller pinned it explicitly, and default the provider dimension too.
if dims.model_name == UNATTRIBUTED and model:
dims = AttributionDimensions(
account=attribution.account,
service=attribution.service,
upstream_api=UNATTRIBUTED,
model_name=UNATTRIBUTED,
team=attribution.team,
group=attribution.group,
company=attribution.company,
)
# Execution identity always wins — descriptive dimensions stay as-is.
if model:
dims.model_name = model
if dims.upstream_api == UNATTRIBUTED and provider:
if provider:
dims.upstream_api = provider

cost_amount, currency = self.price_book.compute_cost(
Expand Down
Loading
Loading