lambda-erp 0.6.16__tar.gz → 0.6.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/PKG-INFO +1 -1
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/attachments.py +9 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/chat.py +205 -16
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/pyproject.toml +1 -1
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/.gitignore +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/LICENSE +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/README.md +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/auth.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/deps.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/errors.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/main.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/oauth.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/pdf.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/providers.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/documents.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/masters.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/mcp.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/services.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/templates/document.html +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/frontend/README.md +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/database.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.18}/terraform/README.md +0 -0
|
@@ -402,6 +402,15 @@ def build_multimodal_content(attachment: dict) -> dict:
|
|
|
402
402
|
},
|
|
403
403
|
}
|
|
404
404
|
if mime in OFFICE_MIME_TYPES:
|
|
405
|
+
# Office bytes are read via the Files API (file_id) — but ONLY the
|
|
406
|
+
# Responses API accepts non-PDF file inputs. The Responses path is the
|
|
407
|
+
# default; only degrade gracefully (no hard 400) if someone has pinned
|
|
408
|
+
# the legacy Chat Completions backend via ERP_CHAT_API=chat.
|
|
409
|
+
if os.environ.get("ERP_CHAT_API", "responses").strip().lower() == "chat":
|
|
410
|
+
return {"type": "text",
|
|
411
|
+
"text": (f"[Attachment “{filename}” is an Office document; the current chat "
|
|
412
|
+
"backend can only read PDFs and images. Convert it to PDF, or ask an "
|
|
413
|
+
"admin to enable the Responses backend.]")}
|
|
405
414
|
file_id = ensure_openai_file(attachment)
|
|
406
415
|
if file_id:
|
|
407
416
|
return {"type": "file", "file": {"file_id": file_id}}
|
|
@@ -2698,6 +2698,206 @@ async def generate_title(
|
|
|
2698
2698
|
ORCHESTRATOR_MODEL = "gpt-5.6-terra"
|
|
2699
2699
|
|
|
2700
2700
|
|
|
2701
|
+
# ---------------------------------------------------------------------------
|
|
2702
|
+
# LLM API selector: Chat Completions (default) vs Responses API
|
|
2703
|
+
#
|
|
2704
|
+
# The Responses API (/v1/responses) is what accepts non-PDF file inputs
|
|
2705
|
+
# (Office documents via input_file), and it does reasoning natively (no
|
|
2706
|
+
# reasoning_effort="none" hack). Migrating the whole loop is risky, so it's
|
|
2707
|
+
# behind a flag and OFF by default. To keep the blast radius tiny, ALL
|
|
2708
|
+
# conversation state stays Chat-Completions-shaped; we translate to/from the
|
|
2709
|
+
# Responses shape only at the single orchestrator call boundary
|
|
2710
|
+
# (_orchestrator_turn), and shim the Responses output back into the
|
|
2711
|
+
# Chat-shaped objects the loop and providers.py already expect.
|
|
2712
|
+
# ---------------------------------------------------------------------------
|
|
2713
|
+
|
|
2714
|
+
def _use_responses_api() -> bool:
|
|
2715
|
+
# Default: the Responses API (needed for Office attachments + native
|
|
2716
|
+
# reasoning). Set ERP_CHAT_API=chat to fall back to Chat Completions.
|
|
2717
|
+
return os.environ.get("ERP_CHAT_API", "responses").strip().lower() != "chat"
|
|
2718
|
+
|
|
2719
|
+
|
|
2720
|
+
# Reasoning effort for the Responses path (native; the Chat path uses "none").
|
|
2721
|
+
_RESPONSES_REASONING_EFFORT = "low"
|
|
2722
|
+
|
|
2723
|
+
|
|
2724
|
+
class _ShimFn:
|
|
2725
|
+
def __init__(self, name, arguments):
|
|
2726
|
+
self.name = name
|
|
2727
|
+
self.arguments = arguments
|
|
2728
|
+
|
|
2729
|
+
|
|
2730
|
+
class _ShimToolCall:
|
|
2731
|
+
def __init__(self, call_id, name, arguments):
|
|
2732
|
+
self.id = call_id
|
|
2733
|
+
self.type = "function"
|
|
2734
|
+
self.function = _ShimFn(name, arguments)
|
|
2735
|
+
|
|
2736
|
+
|
|
2737
|
+
class _ShimMessage:
|
|
2738
|
+
"""Quacks like a Chat Completions `choice.message`."""
|
|
2739
|
+
def __init__(self, content, tool_calls):
|
|
2740
|
+
self.content = content
|
|
2741
|
+
self.tool_calls = tool_calls or None
|
|
2742
|
+
|
|
2743
|
+
|
|
2744
|
+
class _ShimUsageDetails:
|
|
2745
|
+
def __init__(self, cached_tokens):
|
|
2746
|
+
self.cached_tokens = cached_tokens
|
|
2747
|
+
|
|
2748
|
+
|
|
2749
|
+
class _ShimUsage:
|
|
2750
|
+
"""Quacks like a Chat Completions `response.usage` (so cost_of_openai_call
|
|
2751
|
+
and demo_limiter.settle read it unchanged)."""
|
|
2752
|
+
def __init__(self, prompt_tokens, completion_tokens, cached_tokens):
|
|
2753
|
+
self.prompt_tokens = prompt_tokens
|
|
2754
|
+
self.completion_tokens = completion_tokens
|
|
2755
|
+
self.prompt_tokens_details = _ShimUsageDetails(cached_tokens)
|
|
2756
|
+
|
|
2757
|
+
|
|
2758
|
+
def _responses_tools_from_chat(tools: list) -> list:
|
|
2759
|
+
"""Chat tool schema {"type":"function","function":{name,...}} -> the flatter
|
|
2760
|
+
Responses shape {"type":"function","name",...}."""
|
|
2761
|
+
out = []
|
|
2762
|
+
for t in tools or []:
|
|
2763
|
+
fn = t.get("function") if isinstance(t, dict) else None
|
|
2764
|
+
if t.get("type") == "function" and fn:
|
|
2765
|
+
out.append({
|
|
2766
|
+
"type": "function",
|
|
2767
|
+
"name": fn["name"],
|
|
2768
|
+
"description": fn.get("description", ""),
|
|
2769
|
+
"parameters": fn.get("parameters") or {"type": "object", "properties": {}},
|
|
2770
|
+
})
|
|
2771
|
+
else:
|
|
2772
|
+
out.append(t)
|
|
2773
|
+
return out
|
|
2774
|
+
|
|
2775
|
+
|
|
2776
|
+
def _responses_msg_content(content):
|
|
2777
|
+
"""A Chat message `content` -> Responses message content: a plain string
|
|
2778
|
+
passes through unchanged; a multimodal parts list becomes input_* parts
|
|
2779
|
+
(input_text/input_image/input_file). Only user messages carry lists."""
|
|
2780
|
+
if content is None:
|
|
2781
|
+
return ""
|
|
2782
|
+
if isinstance(content, str):
|
|
2783
|
+
return content
|
|
2784
|
+
parts = []
|
|
2785
|
+
for p in content:
|
|
2786
|
+
ptype = p.get("type")
|
|
2787
|
+
if ptype == "text":
|
|
2788
|
+
parts.append({"type": "input_text", "text": p.get("text", "")})
|
|
2789
|
+
elif ptype == "image_url":
|
|
2790
|
+
url = (p.get("image_url") or {}).get("url")
|
|
2791
|
+
parts.append({"type": "input_image", "image_url": url})
|
|
2792
|
+
elif ptype == "file":
|
|
2793
|
+
f = p.get("file") or {}
|
|
2794
|
+
item = {"type": "input_file"}
|
|
2795
|
+
if f.get("file_id"):
|
|
2796
|
+
item["file_id"] = f["file_id"]
|
|
2797
|
+
else:
|
|
2798
|
+
if f.get("filename"):
|
|
2799
|
+
item["filename"] = f["filename"]
|
|
2800
|
+
if f.get("file_data"):
|
|
2801
|
+
item["file_data"] = f["file_data"]
|
|
2802
|
+
parts.append(item)
|
|
2803
|
+
else:
|
|
2804
|
+
parts.append({"type": "input_text", "text": json.dumps(p, default=str)})
|
|
2805
|
+
return parts
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
def _responses_request_from_chat(messages: list) -> tuple:
|
|
2809
|
+
"""Translate Chat `messages` into (instructions, input_items) for Responses.
|
|
2810
|
+
The first system message becomes `instructions`; assistant tool_calls become
|
|
2811
|
+
`function_call` items and tool results become `function_call_output` items."""
|
|
2812
|
+
instructions = None
|
|
2813
|
+
input_items: list = []
|
|
2814
|
+
for m in messages:
|
|
2815
|
+
role = m.get("role")
|
|
2816
|
+
content = m.get("content")
|
|
2817
|
+
if role == "system":
|
|
2818
|
+
flat = content if isinstance(content, str) else " ".join(
|
|
2819
|
+
p.get("text", "") for p in (content or []) if isinstance(p, dict)
|
|
2820
|
+
)
|
|
2821
|
+
if instructions is None:
|
|
2822
|
+
instructions = flat
|
|
2823
|
+
else:
|
|
2824
|
+
input_items.append({"role": "developer", "content": _responses_msg_content(content)})
|
|
2825
|
+
elif role == "assistant":
|
|
2826
|
+
if content:
|
|
2827
|
+
input_items.append({"role": "assistant", "content": _responses_msg_content(content)})
|
|
2828
|
+
for tc in m.get("tool_calls") or []:
|
|
2829
|
+
input_items.append({
|
|
2830
|
+
"type": "function_call",
|
|
2831
|
+
"call_id": tc["id"],
|
|
2832
|
+
"name": tc["function"]["name"],
|
|
2833
|
+
"arguments": tc["function"]["arguments"],
|
|
2834
|
+
})
|
|
2835
|
+
elif role == "tool":
|
|
2836
|
+
input_items.append({
|
|
2837
|
+
"type": "function_call_output",
|
|
2838
|
+
"call_id": m.get("tool_call_id"),
|
|
2839
|
+
"output": m.get("content") or "",
|
|
2840
|
+
})
|
|
2841
|
+
else: # user
|
|
2842
|
+
input_items.append({"role": "user", "content": _responses_msg_content(content)})
|
|
2843
|
+
return instructions, input_items
|
|
2844
|
+
|
|
2845
|
+
|
|
2846
|
+
def _shim_message_from_responses(response) -> _ShimMessage:
|
|
2847
|
+
"""Map a Responses result's output items into a Chat-shaped message."""
|
|
2848
|
+
text_parts, tool_calls = [], []
|
|
2849
|
+
for item in (getattr(response, "output", None) or []):
|
|
2850
|
+
itype = getattr(item, "type", None)
|
|
2851
|
+
if itype == "function_call":
|
|
2852
|
+
tool_calls.append(_ShimToolCall(item.call_id, item.name, item.arguments))
|
|
2853
|
+
elif itype == "message":
|
|
2854
|
+
for c in (getattr(item, "content", None) or []):
|
|
2855
|
+
if getattr(c, "type", None) in ("output_text", "text"):
|
|
2856
|
+
text_parts.append(getattr(c, "text", "") or "")
|
|
2857
|
+
# reasoning items are not echoed back (state is rebuilt from history each turn)
|
|
2858
|
+
text = "".join(text_parts) or (getattr(response, "output_text", "") or "")
|
|
2859
|
+
return _ShimMessage(text, tool_calls)
|
|
2860
|
+
|
|
2861
|
+
|
|
2862
|
+
def _shim_usage_from_responses(usage):
|
|
2863
|
+
if usage is None:
|
|
2864
|
+
return None
|
|
2865
|
+
inp = int(getattr(usage, "input_tokens", 0) or 0)
|
|
2866
|
+
out = int(getattr(usage, "output_tokens", 0) or 0)
|
|
2867
|
+
details = getattr(usage, "input_tokens_details", None)
|
|
2868
|
+
cached = int(getattr(details, "cached_tokens", 0) or 0) if details else 0
|
|
2869
|
+
return _ShimUsage(inp, out, cached)
|
|
2870
|
+
|
|
2871
|
+
|
|
2872
|
+
def _orchestrator_turn(client, messages, tools, max_tokens):
|
|
2873
|
+
"""One orchestrator LLM turn. Returns (message, usage) shaped like Chat
|
|
2874
|
+
Completions, using either the Chat or Responses API per ERP_CHAT_API.
|
|
2875
|
+
Blocking — call via asyncio.to_thread."""
|
|
2876
|
+
if _use_responses_api():
|
|
2877
|
+
instructions, input_items = _responses_request_from_chat(messages)
|
|
2878
|
+
resp = client.responses.create(
|
|
2879
|
+
model=ORCHESTRATOR_MODEL,
|
|
2880
|
+
instructions=instructions,
|
|
2881
|
+
input=input_items,
|
|
2882
|
+
tools=_responses_tools_from_chat(tools),
|
|
2883
|
+
tool_choice="auto",
|
|
2884
|
+
max_output_tokens=max_tokens,
|
|
2885
|
+
reasoning={"effort": _RESPONSES_REASONING_EFFORT},
|
|
2886
|
+
)
|
|
2887
|
+
return _shim_message_from_responses(resp), _shim_usage_from_responses(getattr(resp, "usage", None))
|
|
2888
|
+
resp = client.chat.completions.create(
|
|
2889
|
+
model=ORCHESTRATOR_MODEL,
|
|
2890
|
+
messages=messages,
|
|
2891
|
+
tools=tools,
|
|
2892
|
+
tool_choice="auto",
|
|
2893
|
+
max_completion_tokens=max_tokens,
|
|
2894
|
+
# gpt-5.6-terra rejects function tools on /v1/chat/completions unless
|
|
2895
|
+
# reasoning_effort is "none" (400 otherwise).
|
|
2896
|
+
reasoning_effort="none",
|
|
2897
|
+
)
|
|
2898
|
+
return resp.choices[0].message, getattr(resp, "usage", None)
|
|
2899
|
+
|
|
2900
|
+
|
|
2701
2901
|
async def run_thinking_loop(
|
|
2702
2902
|
messages: list[dict],
|
|
2703
2903
|
on_event,
|
|
@@ -2825,24 +3025,16 @@ async def run_thinking_loop(
|
|
|
2825
3025
|
print(f"[chat_llm] provider=openai model={model_name} session_id={session_id or '-'} iter={iteration + 1}", flush=True)
|
|
2826
3026
|
await on_event({"type": "llm_provider", "provider": "openai", "model": model_name})
|
|
2827
3027
|
try:
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
tool_choice="auto",
|
|
2834
|
-
max_completion_tokens=max_completion,
|
|
2835
|
-
# gpt-5.6-terra rejects function tools on /v1/chat/completions
|
|
2836
|
-
# unless reasoning_effort is "none" (400 otherwise). Tool
|
|
2837
|
-
# calls run without hidden reasoning until this loop migrates
|
|
2838
|
-
# to the Responses API.
|
|
2839
|
-
reasoning_effort="none",
|
|
3028
|
+
# One orchestrator turn via Chat Completions (default) or the
|
|
3029
|
+
# Responses API (ERP_CHAT_API=responses). Returns Chat-shaped
|
|
3030
|
+
# (message, usage) either way, so everything below is unchanged.
|
|
3031
|
+
message, usage = await asyncio.to_thread(
|
|
3032
|
+
_orchestrator_turn, openai_client, messages, build_tools(), max_completion,
|
|
2840
3033
|
)
|
|
2841
3034
|
except Exception as e:
|
|
2842
3035
|
await on_event({"type": "error", "content": f"Error calling LLM: {e}"})
|
|
2843
3036
|
return
|
|
2844
3037
|
|
|
2845
|
-
usage = getattr(response, "usage", None)
|
|
2846
3038
|
demo_limiter.settle(
|
|
2847
3039
|
reservation_id,
|
|
2848
3040
|
actual_cost_usd=cost_of_openai_call(model_name, usage),
|
|
@@ -2859,9 +3051,6 @@ async def run_thinking_loop(
|
|
|
2859
3051
|
if not settled:
|
|
2860
3052
|
demo_limiter.release(reservation_id)
|
|
2861
3053
|
|
|
2862
|
-
choice = response.choices[0]
|
|
2863
|
-
message = choice.message
|
|
2864
|
-
|
|
2865
3054
|
if not message.tool_calls:
|
|
2866
3055
|
content = message.content or ""
|
|
2867
3056
|
messages.append({"role": "assistant", "content": content})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|