Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 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
c381516
fix(api): treat tool/json_schema strict null as omit; require tools f…
seonghobae Aug 16, 2026
5dace0f
fix(api): treat tool.function description/parameters null as omit
seonghobae Aug 16, 2026
55d7a45
fix(api): cap tool.function.description at 1024 characters fail-closed
seonghobae Aug 16, 2026
4f84700
fix(api): treat chat message name empty/whitespace as omit
seonghobae Aug 16, 2026
569f760
fix(api): pop null tool.function optional fields omit-real before proxy
seonghobae Aug 16, 2026
c53774a
fix(api): reject unknown stream_options keys even when null
seonghobae Aug 16, 2026
5d8121b
fix(api): re-land SDK omit honesty on tip (tlp/args/instructions/suff…
seonghobae Aug 16, 2026
7eca5ce
fix(api): persist #668 omit-real (args/instructions/metadata/tlp hoist)
cursoragent Aug 16, 2026
3582cec
fix(api): fail-closed ASCII name charset on tools and json_schema
seonghobae Aug 16, 2026
20fef77
fix(api): accept official Responses text.format structured types
seonghobae Aug 16, 2026
8a18bcd
fix(api): treat Responses truncation auto|disabled as honest no-ops
seonghobae Aug 16, 2026
7a04b6e
chore: re-trigger product gates (Full unit + Semgrep)
seonghobae Aug 16, 2026
3c5bdc5
fix(api): omit-real null json_schema optionals on chat response_format
seonghobae Aug 16, 2026
69da5f6
fix(api): require non-empty tools for tool_choice=required
seonghobae Aug 16, 2026
3371d44
chore: re-trigger product gates (Full unit + Semgrep)
seonghobae Aug 16, 2026
602f8a4
chore: re-trigger product gates (Full unit + Semgrep)
seonghobae Aug 16, 2026
ca646dc
fix(api): fail-closed empty/whitespace OpenAI metadata keys
seonghobae Aug 16, 2026
6efc473
fix(api): casefold service_tier auto/default; omit null routing optio…
seonghobae Aug 16, 2026
7749b2b
fix(api): coerce JS bool 0/1 for store/stream/parallel; digit-string …
seonghobae Aug 16, 2026
bd55b92
fix(api): named reject tool_resources; null/empty omit
seonghobae Aug 16, 2026
c91bbf3
fix(api): digit-string n/best_of; JS bool 0/1 echo/background/logprobs
seonghobae Aug 16, 2026
019fe6f
fix(api): coerce string true/false and numeric control strings
seonghobae Aug 16, 2026
c0494b8
fix(api): whole-float int coerce; digit max_output_tokens; chat stop …
seonghobae Aug 16, 2026
8c230b3
fix(api): casefold encoding_format; Responses stream string-bool; log…
seonghobae Aug 16, 2026
816c263
fix(api): casefold tool_choice and function_call none/auto/required
seonghobae Aug 16, 2026
2a4f7b3
chore: re-trigger product gates (Full unit + Semgrep)
seonghobae Aug 16, 2026
2e97756
fix(api): casefold reasoning_effort none, format types, modalities text
seonghobae Aug 16, 2026
56bfb86
fix(api): coerce include_orchestration_trace string/0-1; omit empty a…
seonghobae Aug 16, 2026
0c08028
fix(api): coerce routing.latency_tolerant and stream_options bool str…
seonghobae Aug 16, 2026
b92fc49
fix(api): casefold mode route/conduct/auto; coerce embeddings dimensi…
seonghobae Aug 16, 2026
269f0d6
fix(api): casefold mode auto|route|conduct; coerce embeddings dimensi…
seonghobae Aug 16, 2026
9313d6d
fix(api): treat max_tool_calls 0 and digit-zero as omit
seonghobae Aug 16, 2026
9e45406
fix(api): coerce whole-float digit strings on optional ints
seonghobae Aug 17, 2026
d44973f
fix(api): omit reasoning.effort none, empty text.format, logprobs 0.0…
seonghobae Aug 17, 2026
5bcb0a1
fix(api): coerce whole-float 0.0/1.0 optional bools for form/JS SDKs
seonghobae Aug 17, 2026
9d026cc
fix(api): coerce message weight digits and tool strict/prefix bool forms
seonghobae Aug 17, 2026
d93b8c9
fix(api): casefold message roles and tool/function types over HTTP
seonghobae Aug 17, 2026
ebc32a2
fix(api): serialize tool_calls arguments objects to JSON text
seonghobae Aug 17, 2026
f3cc2d7
fix(api): strip pad on tool_call ids and function/schema names
seonghobae Aug 17, 2026
136afd6
fix(api): accept Responses flat function tools alongside chat nested
seonghobae Aug 17, 2026
8834bd5
fix(api): accept Responses flat tool_choice name alongside nested
seonghobae Aug 17, 2026
be8ddf1
fix(api): casefold multimodal content part types over HTTP
seonghobae Aug 17, 2026
467417b
fix(api): alias input_text/image content parts; omit empty web_search…
seonghobae Aug 17, 2026
6f61186
fix(api): omit nested-null unsupported objects (prediction/audio/tools)
seonghobae Aug 17, 2026
8562673
fix(api): alias developer message role to system
seonghobae Aug 17, 2026
cd8681a
fix(api): reject padded OpenAI metadata keys fail-closed
seonghobae Aug 17, 2026
468d041
fix(api): coerce metadata bool/int/float values to strings
seonghobae Aug 17, 2026
ecd879a
fix(api): coerce user bool/int/float scalars to strings
seonghobae Aug 17, 2026
eaeaf7d
fix(api): treat reasoning_effort minimal as omit no-op
seonghobae Aug 17, 2026
d1250f5
fix(api): strip+writeback model so tools/Responses passthrough bind
seonghobae Aug 17, 2026
8c491e3
fix(api): coerce logit_bias numeric-string values
seonghobae Aug 17, 2026
333ffcf
fix(api): strip incidental whitespace on logit_bias digit keys
seonghobae Aug 17, 2026
3d030b0
fix(api): support embeddings encoding_format base64
seonghobae Aug 17, 2026
d8f1c69
fix(api): fail-closed sampling controls before tools passthrough
seonghobae Aug 17, 2026
cbc2790
fix(api): accept OpenAI embeddings token-array inputs
seonghobae Aug 17, 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
3 changes: 3 additions & 0 deletions conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
|---|---|---|
| 001-paper-grounded-orchestrator | active | Implement the source-backed orchestration contract with TDD, DDD, and CDD |
| 002-enterprise-design-foundation | active | Add paper-grounded screen design, user stories, REST API, code/DB conventions, and i18n |
| 003-sdk-omit-real-persist | active | #668 re-land accepted SDK omit 200s without write-back. Persist args/instructions/metadata and hoist chat top_logprobs before tools passthrough. Prefer the persist successor over merging the 145-file #668 stack. |
| 003-compatibility-honesty | active | Fail-closed ASCII `[a-zA-Z0-9_-]{1,64}` on `json_schema.name` and `tool.function.name` (`str.isalnum()` leaked Unicode). Re-landed on #686 substrate after parallel tip #685. Do not merge 140-file honesty stacks onto `main`. |
| 003-responses-text-format | active | Official Responses text.format structured types + dual-plane fail-closed. Re-landed on #687 after parallel #681. |
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
173 changes: 163 additions & 10 deletions contextual_orchestrator/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
from .credentials import NotConfigured, get_credential


ChatMessage = dict[str, str]
# content is usually str; multimodal vision messages use OpenAI content-parts lists.
ChatMessage = dict[str, Any]

class BudgetExceededError(RuntimeError):
"""Raised when an operator-configured spend budget is already exhausted."""
Expand Down Expand Up @@ -215,6 +216,10 @@ def __init__(
) -> None:
self.timeout = timeout
self.max_output_tokens = max_output_tokens
self.default_temperature = 0.2
self.default_top_p: float | None = None
self.default_presence_penalty: float | None = None
self.default_frequency_penalty: float | None = None
self.max_retries = max_retries
self.retry_backoff = retry_backoff
self.retry_backoff_cap = retry_backoff_cap
Expand All @@ -230,7 +235,7 @@ def __init__(
@staticmethod
def _build_ssl_context(ca_bundle: str | None, verify_tls: bool) -> ssl.SSLContext:
if not verify_tls:
return ssl._create_unverified_context() # nosec B323 - explicit dev-only provider TLS opt-out.
return ssl._create_unverified_context() # nosec B323 - explicit dev-only provider TLS opt-out. # nosemgrep -- unverified-ssl-context: intentional, default-secure (verify_tls defaults True) dev-only opt-out for self-signed endpoints.
if ca_bundle:
if not os.path.isfile(ca_bundle):
raise ValueError(f"provider CA bundle does not exist: {ca_bundle}")
Expand All @@ -246,9 +251,29 @@ def take_usage(self) -> dict[str, Any] | None:
self._local.usage = None
return usage

def chat(self, agent: ModelAgent, messages: list[ChatMessage], temperature: float = 0.2) -> str:
"""Send messages to a mock or OpenAI-compatible chat endpoint with retries."""
def chat(
self,
agent: ModelAgent,
messages: list[ChatMessage],
temperature: float | None = None,
top_p: float | None = None,
) -> str:
"""Send messages to a mock or OpenAI-compatible chat endpoint with retries.

When ``temperature``/``top_p`` are omitted, ``default_temperature`` and
``default_top_p`` are used so request-scoped Completions sampling can be
applied without threading kwargs through every orchestrator hop.
"""
self._local.usage = None
# Expose the effective sampling knobs for request-path tests / diagnostics.
effective_temperature = self.default_temperature if temperature is None else temperature
effective_top_p = self.default_top_p if top_p is None else top_p
effective_presence = self.default_presence_penalty
effective_frequency = self.default_frequency_penalty
self._local.last_temperature = effective_temperature
self._local.last_top_p = effective_top_p
self._local.last_presence_penalty = effective_presence
self._local.last_frequency_penalty = effective_frequency
if agent.base_url.startswith("mock://"):
return self._mock(agent, messages)

Expand All @@ -262,10 +287,16 @@ def chat(self, agent: ModelAgent, messages: list[ChatMessage], temperature: floa
payload = { # pragma: no cover
"model": agent.model,
"messages": messages,
"temperature": temperature,
"temperature": effective_temperature,
"stream": False,
"max_tokens": self.max_output_tokens,
}
if effective_top_p is not None: # pragma: no cover
payload["top_p"] = effective_top_p
if effective_presence is not None: # pragma: no cover
payload["presence_penalty"] = effective_presence
if effective_frequency is not None: # pragma: no cover
payload["frequency_penalty"] = effective_frequency
return self._send_with_retry(agent, payload)

def _send_with_retry(self, agent: ModelAgent, payload: dict[str, Any]) -> str:
Expand Down Expand Up @@ -307,7 +338,7 @@ def _send(self, agent: ModelAgent, payload: dict[str, Any]) -> str:

def _open_provider(self, request: urllib.request.Request) -> Any:
"""Open a provider request built from a validated provider URL."""
return urllib.request.urlopen( # nosec B310 - request URL comes from _provider_url after provider validation.
return urllib.request.urlopen( # nosec B310 - request URL comes from _provider_url after provider validation. # nosemgrep -- dynamic-urllib-use: URL is built by _provider_url after scheme/host validation; egress to loopback/private/reserved is blocked.
request,
timeout=self.timeout,
context=self._ssl_context,
Expand Down Expand Up @@ -418,7 +449,19 @@ def _mock_raw(
"""Mock full provider response for tests; echoes forwarded params so passthrough is assertable."""
echoed = {
key: payload[key]
for key in ("model", "response_format", "tools", "tool_choice", "temperature", "max_tokens")
for key in (
"model",
"response_format",
"tools",
"tool_choice",
"temperature",
"max_tokens",
"instructions",
"metadata",
"messages",
"top_logprobs",
"text",
)
if key in payload
}
if endpoint.strip("/") == "responses":
Expand Down Expand Up @@ -634,6 +677,9 @@ def _coerce_input_text(value: Any) -> str:
if isinstance(item, str):
parts.append(item)
elif isinstance(item, dict):
# OpenAI content-parts: {"type": "text", "text": "..."}
if isinstance(item.get("text"), str):
parts.append(item["text"])
content = item.get("content")
if isinstance(content, str):
parts.append(content)
Expand All @@ -644,6 +690,15 @@ def _coerce_input_text(value: Any) -> str:
return " ".join(parts)


def _coerce_message_content_text(content: Any) -> str:
"""Best-effort plain text from chat message content (string or content-parts)."""
if isinstance(content, str):
return content
if isinstance(content, list):
return _coerce_input_text(content)
return ""


def load_agents(path: str) -> list[ModelAgent]: # pragma: no cover
"""Load model agent definitions from an agents JSON file."""
with open(path, encoding="utf-8") as handle:
Expand Down Expand Up @@ -879,7 +934,14 @@ def _reload_state(self) -> None:

# Orchestration-only body keys that must not be forwarded to the provider.
_ORCHESTRATION_ONLY_KEYS = frozenset(
{"orchestration", "orchestration_mode", "mode", "include_orchestration_trace"}
{
"orchestration",
"orchestration_mode",
"mode",
"include_orchestration_trace",
"attribution",
"routing",
}
)

def proxy_completion(
Expand All @@ -897,7 +959,23 @@ def proxy_completion(
text = self._latest_user_text(messages)
else:
text = _coerce_input_text(body.get("input"))
agent = self._select_agent(text, "worker")
requested_model = body.get("model")
# When the client names a model, resolve a pool agent that actually serves
# that model id. Silent rewrite to an unrelated agent.model is a commercial
# honesty failure for OpenAI SDKs (passthrough tools/Responses paths).
if isinstance(requested_model, str) and requested_model.strip():
matched = [
agent
for agent in self.agents
if not getattr(agent, "disabled", False) and agent.model == requested_model
]
if not matched:
raise ValueError(
f"model {requested_model!r} is not available in the agent pool"
)
agent = matched[0]
else:
agent = self._select_agent(text, "worker")
upstream = {
key: value
for key, value in body.items()
Expand Down Expand Up @@ -1600,7 +1678,13 @@ def _needs_workflow(self, text: str) -> bool:
return hits >= self.policy.conduct_hint_threshold or len(text) > 700

def _latest_user_text(self, messages: list[ChatMessage]) -> str:
return next((m.get("content", "") for m in reversed(messages) if m.get("role") == "user"), "") # pragma: no cover
for message in reversed(messages):
if message.get("role") != "user":
continue
text = _coerce_message_content_text(message.get("content", ""))
if text:
return text
return "" # pragma: no cover

def _model_judge_verification(self, task: str, fallback: dict[str, Any]) -> dict[str, Any]:
"""Ask a model to judge the verifier report (fixes term-matching false negatives).
Expand Down Expand Up @@ -1695,6 +1779,52 @@ def list_agents(self, page_number: int = 1, page_size: int = 10) -> list[dict[st
end = start + page_size
return [self._agent_to_admin_payload(agent) for agent in self.agents[start:end]]

def list_openai_models(self) -> dict[str, Any]:
"""Return an OpenAI-compatible ``/v1/models`` list from the agent pool.

Buyers discover selectable model ids without admin-scope agent pool access.
Each enabled agent model appears once; gateway default
``contextual-orchestrator`` is always first.
"""
created = 1_700_000_000 # stable epoch so list responses are deterministic
data: list[dict[str, Any]] = [
{
"id": "contextual-orchestrator",
"object": "model",
"created": created,
"owned_by": "contextual-orchestrator",
}
]
seen: set[str] = {"contextual-orchestrator"}
for agent in self.agents:
if agent.disabled:
continue
model_id = str(agent.model).strip()
if not model_id or model_id in seen:
continue
seen.add(model_id)
data.append(
{
"id": model_id,
"object": "model",
"created": created,
"owned_by": agent.provider_name
or self._infer_provider_name(agent.base_url)
or "agent_pool",
}
)
return {"object": "list", "data": data}

def get_openai_model(self, model_id: str) -> dict[str, Any]:
"""Return one OpenAI model object or raise ``KeyError`` when unknown."""
wanted = (model_id or "").strip()
if not wanted:
raise KeyError(model_id)
for item in self.list_openai_models()["data"]:
if item["id"] == wanted:
return item
raise KeyError(model_id)

def list_recent_runs(self, page_number: int = 1, page_size: int = 10) -> list[dict[str, Any]]:
"""Return a paginated list of recent workflow run records."""
if page_number < 1 or page_size < 1: # pragma: no cover
Expand Down Expand Up @@ -8517,6 +8647,29 @@ def chat_completion_response(
}


def text_completion_response(
result: dict[str, Any],
model: str = "contextual-orchestrator",
usage: dict[str, int] | None = None,
) -> dict[str, Any]: # pragma: no cover
"""Wrap orchestration output as OpenAI legacy ``text_completion`` (``/v1/completions``)."""
return {
"id": f"cmpl-{int(time.time() * 1000)}",
"object": "text_completion",
"created": int(time.time()),
"model": model,
"choices": [
{
"index": 0,
"text": result["answer"],
"logprobs": None,
"finish_reason": "stop",
}
],
"usage": usage or {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
}


_STREAM_CHUNK_SIZE = 32


Expand Down
Loading
Loading