Repository navigation
feat(decisions): add OpenAI as a Decisions provider behind a shared decisions format - #45214
Conversation
…format at /v1/decisions The System One request format moves to /v1/systemone and /systemone. /v1/decisions and /decisions now accept the OpenAI Decisions API format, translate it into a System One request, route it through the same pipeline, and translate the answers back.
/v1/decisions accepted up to 200 questions, but the System One request it translates to takes at most 128, so 129 to 200 questions failed with an internal validation error. Both now share MAX_DECISION_QUESTIONS.
System One requests to openai/ models are translated to OpenAI's Decisions shape on the way out and OpenAI's answers are translated back, so both /v1/systemone and /v1/decisions can route to gpt-6-luna.
|
|
@greptileai please re-review now that the OpenAI wire translation lives under llms/openai as you suggested |
Merging this PR will not alter performance
Comparing |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…s IR System One and OpenAI-format bodies now convert to one internal representation, and each provider translates it to its own wire format. OpenAI deployments receive the caller's messages, images, names, typed choice values, level descriptions and safety_identifier unchanged. System One providers return a 400 for image input. Cached and cache-write tokens are priced on both response shapes, and provider response extras survive the round trip.
|
@greptileai please re-review the latest commit, which routes both Decisions request formats through one shared format |
|
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Answer name is required on parse
- Defaulted OpenAI answer name to None so provider responses that omit the key on unnamed answers parse instead of raising.
Or push these changes by commenting:
@cursor push 6d895d3d5b
Preview (6d895d3d5b)
diff --git a/litellm/types/decisions.py b/litellm/types/decisions.py
--- a/litellm/types/decisions.py
+++ b/litellm/types/decisions.py
@@ -235,7 +235,7 @@
class OpenAIPredicateAnswer(LiteLLMPydanticObjectBase):
type: Literal["predicate"] = "predicate"
- name: str | None
+ name: str | None = None
probability: float
model_config = ConfigDict(frozen=True)
@@ -250,7 +250,7 @@
class OpenAIChoiceAnswer(LiteLLMPydanticObjectBase):
type: Literal["choice"] = "choice"
- name: str | None
+ name: str | None = None
choice: str | bool
probabilities: tuple[OpenAIChoiceProbability, ...]
confidence: float
@@ -268,7 +268,7 @@
class OpenAIScoreAnswer(LiteLLMPydanticObjectBase):
type: Literal["score"] = "score"
- name: str | None
+ name: str | None = None
score: float
probabilities: tuple[OpenAIScoreProbability, ...]
confidence: float
@@ -278,7 +278,7 @@
class OpenAIRefusalAnswer(LiteLLMPydanticObjectBase):
type: Literal["refusal"] = "refusal"
- name: str | None
+ name: str | None = None
model_config = ConfigDict(frozen=True)
diff --git a/tests/unit/llms/openai/decisions/test_openai_decisions_transformation.py b/tests/unit/llms/openai/decisions/test_openai_decisions_transformation.py
--- a/tests/unit/llms/openai/decisions/test_openai_decisions_transformation.py
+++ b/tests/unit/llms/openai/decisions/test_openai_decisions_transformation.py
@@ -100,6 +100,36 @@
assert ir_to_openai_response(parsed, ir, "requested").model_dump(mode="json") == _OPENAI_RESPONSE
+def test_parse_response_accepts_answers_that_omit_name() -> None:
+ ir: Final = _openai_ir(_OPENAI_REQUEST)
+ payload: Final = {
+ **_OPENAI_RESPONSE,
+ "answers": [
+ {"type": "predicate", "probability": 0.95},
+ {
+ "type": "choice",
+ "choice": True,
+ "probabilities": [{"value": True, "probability": 0.9}, {"value": "escalate", "probability": 0.1}],
+ "confidence": 0.8,
+ },
+ {
+ "type": "score",
+ "score": 0.7,
+ "probabilities": [
+ {"value": 0, "label": "minor", "probability": 0.3},
+ {"value": 1, "label": "major", "probability": 0.7},
+ ],
+ "confidence": 0.6,
+ },
+ {"type": "refusal"},
+ ],
+ }
+
+ parsed: Final = OPENAI_DECISIONS_ENDPOINT.parse_response(payload, ir)
+
+ assert ir_to_openai_response(parsed, ir, "requested").model_dump(mode="json") == _OPENAI_RESPONSE
+
+
def test_answers_openai_did_not_return_are_refusals() -> None:
ir: Final = _openai_ir(_OPENAI_REQUEST)
payload: Final = {**_OPENAI_RESPONSE, "answers": [_PREDICATE_ANSWER]}You can send follow-ups to the cloud agent here.
…point # Conflicts: # litellm/types/decisions.py
…ns_provider Ports the openai Decisions provider and the shared IR onto main's BaseDecisionsConfig and shared HTTP handler dispatch
…ns when requested
… litellm_openai_decisions_provider # Conflicts: # litellm/proxy/decisions_endpoints/endpoints.py # litellm/types/decisions.py
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c19ada6. Configure here.
…ons_provider # Conflicts: # litellm/proxy/decisions_endpoints/endpoints.py # litellm/types/decisions.py # tests/unit/proxy/decisions_endpoints/test_endpoints.py
…and the openai Decisions provider to rc/1.105.0 (#44236, #45184, #45214) (#45189) * feat(decisions): add unified /v1/decisions endpoint for Jev-compatible providers (#44236) Backport of #44236 to rc/1.105.0. Cherry-picked from 8b1990b (main). * feat(decisions): serve System One format at /v1/systemone and OpenAI format at /v1/decisions Backport of #45184 to rc/1.105.0. Cherry-picked from 3339469 (litellm_systemone_endpoint). The playground System One tab is not on this line, so its UI hunks are dropped. * fix(decisions): import assert_never from typing_extensions for Python 3.10 Backport of the #45184 follow-up to rc/1.105.0. Cherry-picked from 29e7154 (litellm_systemone_endpoint). * fix(decisions): cap OpenAI-format questions at the System One limit Backport of the #45184 follow-up to rc/1.105.0. Cherry-picked from 7ef1329 (litellm_systemone_endpoint). * fix(decisions): return 400 for bodies that are not JSON Backport of the #45184 follow-up to rc/1.105.0. Cherry-picked from b6bb885 (litellm_systemone_endpoint). * test(decisions): send System One bodies to /v1/systemone in integration tests Backport of the #45184 follow-up to rc/1.105.0. Cherry-picked from 77872f6 (litellm_systemone_endpoint). * feat(decisions): add OpenAI as a Decisions provider System One requests to openai/ models are translated to OpenAI's Decisions shape on the way out and OpenAI's answers are translated back, so both /v1/systemone and /v1/decisions can route to gpt-6-luna. (cherry picked from commit b94bbbc) * refactor(decisions): move the OpenAI wire translation into llms/openai (cherry picked from commit d209a85) * feat(decisions): route both request formats through a shared decisions IR System One and OpenAI-format bodies now convert to one internal representation, and each provider translates it to its own wire format. OpenAI deployments receive the caller's messages, images, names, typed choice values, level descriptions and safety_identifier unchanged. System One providers return a 400 for image input. Cached and cache-write tokens are priced on both response shapes, and provider response extras survive the round trip. (cherry picked from commit 0dd207f) * fix(decisions): keep provider fields inside System One answers (cherry picked from commit 1348de8) * fix(decisions): load on Python 3.10 and 3.11 and bill cached tokens once under custom pricing Decisions IR dataclasses used a mappingproxy default, which is only hashable from Python 3.12, so import litellm failed on 3.10 and 3.11. Decisions usage now reaches cost_per_token as a Usage with prompt_tokens_details, so custom pricing no longer adds cached and cache-write tokens on top of input_tokens that already include them (cherry picked from commit 39012f1) * fix(decisions): honor litellm OpenAI key and base settings for the openai provider OpenAI decisions now resolve the key from litellm.api_key, litellm.openai_key, then OPENAI_API_KEY, and the base from litellm.api_base, OPENAI_BASE_URL, then OPENAI_API_BASE, matching other OpenAI calls. Providers own their configured key and base lookup through the endpoint config. (cherry picked from commit e465919) * test(decisions): clear every OpenAI base setting in the proxy OpenAI deployment test (cherry picked from commit a2951b3) * fix(decisions): send OpenAI instructions for System One questions without them and reject single-option questions (cherry picked from commit e4081f7) * fix(decisions): return guardrail_information on OpenAI-format decisions when requested Backport of the #45184 and #45214 guardrail fix. Cherry-picked from 855f0bb (litellm_systemone_endpoint) as merged into #45214 at c19ada6 (litellm_openai_decisions_provider). --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…04.2) (#374) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/berriai/litellm](https://images.chainguard.dev/directory/image/wolfi-base/overview) ([source](https://github.com/BerriAI/litellm)) | patch | `v1.104.0` → `v1.104.2` | --- ### Release Notes <details> <summary>BerriAI/litellm (ghcr.io/berriai/litellm)</summary> ### [`v1.104.2`](https://github.com/BerriAI/litellm/releases/tag/v1.104.2) [Compare Source](BerriAI/litellm@v1.104.1...v1.104.2) #### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.104.2 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.104.2/cosign.pub \ ghcr.io/berriai/litellm:v1.104.2 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** #### What's Changed - feat(decisions): backport /v1/systemone, OpenAI-format /v1/decisions and the OpenAI Decisions provider to stable/1.104.x for v1.104.2 ([#​44236](BerriAI/litellm#44236), [#​45184](BerriAI/litellm#45184), [#​45214](BerriAI/litellm#45214)) by [@​mateo-berri](https://github.com/mateo-berri) in [#​45190](BerriAI/litellm#45190) **Full Changelog**: <BerriAI/litellm@v1.104.1...v1.104.2> ### [`v1.104.1`](https://github.com/BerriAI/litellm/releases/tag/v1.104.1) [Compare Source](BerriAI/litellm@v1.104.0...v1.104.1) #### Verify Docker Image Signature All LiteLLM Docker images are signed with [cosign](https://docs.sigstore.dev/cosign/overview/). Every release is signed with the same key introduced in [commit `0112e53`](BerriAI/litellm@0112e53). **Verify using the pinned commit hash (recommended):** A commit hash is cryptographically immutable, so this is the strongest way to ensure you are using the original signing key: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/0112e53046018d726492c814b3644b7d376029d0/cosign.pub \ ghcr.io/berriai/litellm:v1.104.1 ``` **Verify using the release tag (convenience):** Tags are protected in this repository and resolve to the same key. This option is easier to read but relies on tag protection rules: ```bash cosign verify \ --key https://raw.githubusercontent.com/BerriAI/litellm/v1.104.1/cosign.pub \ ghcr.io/berriai/litellm:v1.104.1 ``` Expected output: ``` The following checks were performed on each of these signatures: - The cosign claims were validated - The signatures were verified against the specified public key ``` *** #### What's Changed - chore(release): refresh dependencies on stable/1.104.x and cut 1.104.1 by [@​yuneng-berri](https://github.com/yuneng-berri) in [#​44777](BerriAI/litellm#44777) - chore(docker): bump pgbouncer to 1.26.0 by [@​devin-ai-integration](https://github.com/devin-ai-integration)\[bot] in [#​45012](BerriAI/litellm#45012) **Full Changelog**: <BerriAI/litellm@v1.104.0...v1.104.1> </details> --- ### Configuration 📅 **Schedule**: (in timezone America/New_York) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4xMzMuMCIsInVwZGF0ZWRJblZlciI6IjQ0LjEzMy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/374
…pes left dead by #45214 (#45442) #45214 routed both decision formats through the shared decisions IR in litellm/llms/base_llm/decisions/transformation.py and litellm/types/decisions.py. That left litellm/llms/base_llm/decisions/systemone.py (to_system_one_request, question_keys, to_decisions_response, the SystemOne* models and their adapter) and litellm/types/openai_decisions.py with no importer outside their own unit tests, so both modules and both test files go. Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
…pes left dead by BerriAI#45214 (BerriAI#45442) BerriAI#45214 routed both decision formats through the shared decisions IR in litellm/llms/base_llm/decisions/transformation.py and litellm/types/decisions.py. That left litellm/llms/base_llm/decisions/systemone.py (to_system_one_request, question_keys, to_decisions_response, the SystemOne* models and their adapter) and litellm/types/openai_decisions.py with no importer outside their own unit tests, so both modules and both test files go. Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com> (cherry picked from commit 0e48048)

TLDR
Problem this solves:
gpt-6-luna) was not a Decisions provideropenai/gpt-6-lunadeployments got a 400 on both Decisions routes/v1/decisionsrejectedinput_imageparts with a 400 for every providerHow it solves it:
openaiprovider callshttps://api.openai.com/v1/decisions, passing OpenAI-format input and questions through field for field/v1/decisionsto OpenAI keeps images, multi-message input,safety_identifier, names and refusalsgpt-6-lunaprice row now lists/v1/decisionsopenaiprovider readslitellm.api_key,litellm.openai_key,litellm.api_base,OPENAI_BASE_URLandOPENAI_API_BASEin the same order as other OpenAI callsinstructionsget a short default one for OpenAI, and single-option questions get a clear 400 before any upstream callBaseDecisionsConfigand shared HTTP handler dispatch from refactor(decisions): dispatch /v1/decisions through provider configs and the shared HTTP handler #45130:OpenAIDecisionsConfigis registered inProviderConfigManager, and every config translates the shared format throughtransform_decisions_requestandparse_responseIntentional product change:
/v1/systemoneresponses only carry answers for the questions the caller asked; extra answer keys a provider returns are dropped, while extra fields inside each answer are keptStacked on #45184, so until that merges the diff also shows its commits. The new work is the commits after 77872f6
User Flow
Before: a proxy admin who adds
openai/gpt-6-lunaas a decider gets a 400 on every Decisions callmodel: openai/gpt-6-lunaaslunanext to their existingtypesafe/jev-latestasjev"model": "luna",inputmessages and aquestionslistUnknown Decisions provider 'openai', the same on POST http://localhost:4000/v1/systemoneinput_imagepart fails validation with a 400, even forjevAfter: both models answer on both routes, and images reach OpenAI
model: openai/gpt-6-lunaaslunanext to their existingtypesafe/jev-latestasjevanswerslist carrying their question names, and/v1/systemonereturns ananswersmap keyed by nameinput_imagepart gets a real answer fromluna, whilejevreturns 400 saying it accepts text input onlyx-litellm-response-costheaderPre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review)
Screenshots / Proof of Fix
Proxy started with
python -m litellm.proxy.proxy_cli --config both_config.yaml --port 4000, calling OpenAI and Typesafe for realluna_systemone_body.json(jev_systemone_body.jsonis the same with"model": "jev"):{ "model": "luna", "state": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today.", "questions": { "is_refund_request": { "type": "noul", "instructions": "Is the customer asking for a refund?" }, "urgency": { "type": "choice", "instructions": "How urgent is this?", "criteria": { "low": "can wait a week", "high": "needs action today" } }, "frustration": { "type": "score", "instructions": "How frustrated is the customer?", "criteria": [ "calm", "mildly annoyed", "angry" ] } } }luna_openai_body.json(jev_openai_body.jsonis the same with"model": "jev"):{ "model": "luna", "input": [ { "role": "user", "type": "message", "content": [ { "type": "input_text", "text": "Customer wrote: I was charged twice for my order #4411 and want one of the charges refunded today." } ] } ], "questions": [ { "type": "predicate", "name": "is_refund_request", "instructions": "Is the customer asking for a refund?" }, { "type": "choice", "name": "needs_human", "instructions": "Should a human agent take over?", "choices": [ { "value": true, "description": "billing dispute that needs a person" }, { "value": false } ] }, { "type": "score", "name": "frustration", "instructions": "How frustrated is the customer?", "levels": [ { "label": "calm" }, { "label": "mildly annoyed" }, { "label": "angry", "description": "uses hostile language" } ] } ] }luna_image_body.json(jev_image_body.jsonis the same with"model": "jev"):{ "model": "luna", "input": [ { "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "Here is the swatch the customer uploaded." }, { "type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgoAAAANSU...(64x64 solid red PNG)", "detail": "low" } ] } ], "questions": [ { "type": "predicate", "name": "is_red", "instructions": "Is the swatch mostly red?" }, { "type": "choice", "name": "color", "instructions": "Which color dominates the swatch?", "choices": [ { "value": "red" }, { "value": "green" }, { "value": "blue" } ] } ], "safety_identifier": "proof-user-1" }Before (77872f6)
luna, System One format at /v1/systemone
curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_systemone_body.jsonluna, OpenAI format at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_openai_body.jsonluna, OpenAI format with an image at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_image_body.jsonjev, System One format at /v1/systemone
curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_systemone_body.jsonjev, OpenAI format at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_openai_body.jsonjev, OpenAI format with an image at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_image_body.jsonAfter (39012f1)
luna, System One format at /v1/systemone
curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_systemone_body.jsonluna, OpenAI format at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_openai_body.jsonluna, OpenAI format with an image at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @luna_image_body.jsonjev, System One format at /v1/systemone
curl -sS -i localhost:4000/v1/systemone -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_systemone_body.jsonjev, OpenAI format at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_openai_body.jsonjev, OpenAI format with an image at /v1/decisions
curl -sS -i localhost:4000/v1/decisions -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H 'Content-Type: application/json' -d @jev_image_body.jsonCustom pricing (39012f1)
A second proxy with per-token prices set on the deployment, called twice with a ~1.8k-token input (
cost_openai_body.json) to see whether OpenAI reports cached tokensBilling matches the configured rates. OpenAI and Typesafe both returned 0 cached tokens on every repeat (5 and 3 identical calls), so the cached-token path is covered by
test_custom_token_pricing_bills_cached_decisions_input_tokens_once, which fails on 85f9121 and passes hereQA notes from the run:
1reaches OpenAI astruetrueand"true"together are rejected for OpenAI toolunareturns OpenAI's 400,jevreturns 200output_tokensas 0 forluna/v1/systemoneusage has nototal_tokensluna: OpenAI 400 nameschoicesOpenAI key from
litellm_settings(e465919)The proxy runs with no
OPENAI_API_KEYin its environment and the key set only throughlitellm_settings.openai_key, which other OpenAI calls already honorBefore, at 39012f1, the provider never looked at
litellm.openai_keyAfter, at e465919, the same request reaches OpenAI and gets a real answer
System One questions without
instructions(e4081f7)Same config as above. Before, at a2951b3, OpenAI rejected the translated request for the criteria-only choice and score questions, and the single-option question cost an upstream round trip
After, at e4081f7, the criteria-only questions get real answers from OpenAI and the single-option question gets a litellm 400 that says why
After merging main's provider config dispatch (48f1a9c)
Same luna config and bodies as the After runs above, on the tip that ports this PR onto #45130. Both routes still reach OpenAI and bill, and the
litellm_settingskey and single-option runs from the previous section were repeated here with the same outputType
New Feature
Caveats (if any)
Medium
gpt-6-lunatoken rates until it doesopenaideployments, since OpenAI needs at least 2Low
criteriaare folded into the predicate's instructions as "Answer true when: ..." linesinstructionsare sent to OpenAI with "Which choice best fits the input?" or "Which level best fits the input?"latency_msonly show up on/v1/systemone, not in OpenAI-format answers1is coerced totrueby request validation (from feat(decisions): serve System One format at /v1/systemone and OpenAI format at /v1/decisions #45184), where OpenAI itself would return 400trueand"true"in one question get a 400 from the System One key check even when routed to OpenAI, which accepts themFinal Attestation
Note
Medium Risk
Touches request routing, cross-format translation, and billing for a new provider API; mis-translation or usage mapping could cause wrong answers or spend, but behavior is heavily tested.
Overview
Adds OpenAI as a Decisions provider and splits the proxy into two request shapes:
/v1/systemone(state + keyed noul/choice/score questions) and/v1/decisions(OpenAI-styleinput, question list,safety_identifier, including images).decisions/adecisionsnow validate either format, reject mixingstateandinput, normalize through a shared IR, let each provider build the upstream body viatransform_decisions_request, then map the IR response back to System One or OpenAI answers.OpenAIDecisionsConfigtargetshttps://api.openai.com/v1/decisions; Jev-style providers still use/v1/systemonewire bodies. Unsupported cases (e.g. images to text-only providers, single-option choice/score for OpenAI) fail with 400 before HTTP.Cost and logging understand
OpenAIDecisionResponseusage (cached / cache-write input tokens).gpt-6-lunapricing lists/v1/decisions. Gateway allowlists, lazy routes, auth, integration tests, and the playground default shift legacy Jev traffic to/v1/systemone;/v1/decisionsstays available for pass-through when configured.Reviewed by Cursor Bugbot for commit c19ada6. Bugbot is set up for automated code reviews on this repo. Configure here.