lambda-erp 0.6.16__tar.gz → 0.6.17__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.17}/PKG-INFO +1 -1
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/attachments.py +9 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/chat.py +203 -16
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/pyproject.toml +1 -1
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/.gitignore +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/LICENSE +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/README.md +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/auth.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/bootstrap.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/demo_limits.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/deps.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/errors.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/main.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/oauth.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/pdf.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/providers.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/remarks_md.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/accounting.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/admin.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/analytics.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/bank_reconciliation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/chat_api.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/documents.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/masters.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/mcp.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/proposals.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/reports.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/routers/setup.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/services.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/templates/document.html +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/api/templates/proposal.html +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/docs/agents/README.md +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/frontend/LICENSE +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/frontend/README.md +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/frontend/src/api/client.ts +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/bank_transaction.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/budget.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/chart_of_accounts.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/general_ledger.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/journal_entry.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/payment_entry.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/pos_invoice.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/purchase_invoice.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/revaluation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/sales_invoice.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/engine.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/pack.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/ch.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/de_common.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/de_skr03.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/de_skr04.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/packs/generic.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/profiles.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/setup/spine.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/accounting/subscription.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/buying/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/buying/purchase_order.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/currency.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/defaults.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/pricing_rule.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/controllers/taxes_and_totals.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/database.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/exceptions.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/hooks.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/model.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/proposal.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/quotation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/selling/sales_order.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/simulation.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/__init__.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/delivery_note.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/purchase_receipt.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/stock_entry.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/stock/stock_ledger.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/lambda_erp/utils.py +0 -0
- {lambda_erp-0.6.16 → lambda_erp-0.6.17}/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. On the default Chat
|
|
407
|
+
# Completions backend, degrade gracefully instead of triggering a hard
|
|
408
|
+
# 400 (flip ERP_CHAT_API=responses to actually read Office files).
|
|
409
|
+
if os.environ.get("ERP_CHAT_API", "chat").strip().lower() != "responses":
|
|
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,204 @@ 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
|
+
return os.environ.get("ERP_CHAT_API", "chat").strip().lower() == "responses"
|
|
2716
|
+
|
|
2717
|
+
|
|
2718
|
+
# Reasoning effort for the Responses path (native; the Chat path uses "none").
|
|
2719
|
+
_RESPONSES_REASONING_EFFORT = "low"
|
|
2720
|
+
|
|
2721
|
+
|
|
2722
|
+
class _ShimFn:
|
|
2723
|
+
def __init__(self, name, arguments):
|
|
2724
|
+
self.name = name
|
|
2725
|
+
self.arguments = arguments
|
|
2726
|
+
|
|
2727
|
+
|
|
2728
|
+
class _ShimToolCall:
|
|
2729
|
+
def __init__(self, call_id, name, arguments):
|
|
2730
|
+
self.id = call_id
|
|
2731
|
+
self.type = "function"
|
|
2732
|
+
self.function = _ShimFn(name, arguments)
|
|
2733
|
+
|
|
2734
|
+
|
|
2735
|
+
class _ShimMessage:
|
|
2736
|
+
"""Quacks like a Chat Completions `choice.message`."""
|
|
2737
|
+
def __init__(self, content, tool_calls):
|
|
2738
|
+
self.content = content
|
|
2739
|
+
self.tool_calls = tool_calls or None
|
|
2740
|
+
|
|
2741
|
+
|
|
2742
|
+
class _ShimUsageDetails:
|
|
2743
|
+
def __init__(self, cached_tokens):
|
|
2744
|
+
self.cached_tokens = cached_tokens
|
|
2745
|
+
|
|
2746
|
+
|
|
2747
|
+
class _ShimUsage:
|
|
2748
|
+
"""Quacks like a Chat Completions `response.usage` (so cost_of_openai_call
|
|
2749
|
+
and demo_limiter.settle read it unchanged)."""
|
|
2750
|
+
def __init__(self, prompt_tokens, completion_tokens, cached_tokens):
|
|
2751
|
+
self.prompt_tokens = prompt_tokens
|
|
2752
|
+
self.completion_tokens = completion_tokens
|
|
2753
|
+
self.prompt_tokens_details = _ShimUsageDetails(cached_tokens)
|
|
2754
|
+
|
|
2755
|
+
|
|
2756
|
+
def _responses_tools_from_chat(tools: list) -> list:
|
|
2757
|
+
"""Chat tool schema {"type":"function","function":{name,...}} -> the flatter
|
|
2758
|
+
Responses shape {"type":"function","name",...}."""
|
|
2759
|
+
out = []
|
|
2760
|
+
for t in tools or []:
|
|
2761
|
+
fn = t.get("function") if isinstance(t, dict) else None
|
|
2762
|
+
if t.get("type") == "function" and fn:
|
|
2763
|
+
out.append({
|
|
2764
|
+
"type": "function",
|
|
2765
|
+
"name": fn["name"],
|
|
2766
|
+
"description": fn.get("description", ""),
|
|
2767
|
+
"parameters": fn.get("parameters") or {"type": "object", "properties": {}},
|
|
2768
|
+
})
|
|
2769
|
+
else:
|
|
2770
|
+
out.append(t)
|
|
2771
|
+
return out
|
|
2772
|
+
|
|
2773
|
+
|
|
2774
|
+
def _responses_msg_content(content):
|
|
2775
|
+
"""A Chat message `content` -> Responses message content: a plain string
|
|
2776
|
+
passes through unchanged; a multimodal parts list becomes input_* parts
|
|
2777
|
+
(input_text/input_image/input_file). Only user messages carry lists."""
|
|
2778
|
+
if content is None:
|
|
2779
|
+
return ""
|
|
2780
|
+
if isinstance(content, str):
|
|
2781
|
+
return content
|
|
2782
|
+
parts = []
|
|
2783
|
+
for p in content:
|
|
2784
|
+
ptype = p.get("type")
|
|
2785
|
+
if ptype == "text":
|
|
2786
|
+
parts.append({"type": "input_text", "text": p.get("text", "")})
|
|
2787
|
+
elif ptype == "image_url":
|
|
2788
|
+
url = (p.get("image_url") or {}).get("url")
|
|
2789
|
+
parts.append({"type": "input_image", "image_url": url})
|
|
2790
|
+
elif ptype == "file":
|
|
2791
|
+
f = p.get("file") or {}
|
|
2792
|
+
item = {"type": "input_file"}
|
|
2793
|
+
if f.get("file_id"):
|
|
2794
|
+
item["file_id"] = f["file_id"]
|
|
2795
|
+
else:
|
|
2796
|
+
if f.get("filename"):
|
|
2797
|
+
item["filename"] = f["filename"]
|
|
2798
|
+
if f.get("file_data"):
|
|
2799
|
+
item["file_data"] = f["file_data"]
|
|
2800
|
+
parts.append(item)
|
|
2801
|
+
else:
|
|
2802
|
+
parts.append({"type": "input_text", "text": json.dumps(p, default=str)})
|
|
2803
|
+
return parts
|
|
2804
|
+
|
|
2805
|
+
|
|
2806
|
+
def _responses_request_from_chat(messages: list) -> tuple:
|
|
2807
|
+
"""Translate Chat `messages` into (instructions, input_items) for Responses.
|
|
2808
|
+
The first system message becomes `instructions`; assistant tool_calls become
|
|
2809
|
+
`function_call` items and tool results become `function_call_output` items."""
|
|
2810
|
+
instructions = None
|
|
2811
|
+
input_items: list = []
|
|
2812
|
+
for m in messages:
|
|
2813
|
+
role = m.get("role")
|
|
2814
|
+
content = m.get("content")
|
|
2815
|
+
if role == "system":
|
|
2816
|
+
flat = content if isinstance(content, str) else " ".join(
|
|
2817
|
+
p.get("text", "") for p in (content or []) if isinstance(p, dict)
|
|
2818
|
+
)
|
|
2819
|
+
if instructions is None:
|
|
2820
|
+
instructions = flat
|
|
2821
|
+
else:
|
|
2822
|
+
input_items.append({"role": "developer", "content": _responses_msg_content(content)})
|
|
2823
|
+
elif role == "assistant":
|
|
2824
|
+
if content:
|
|
2825
|
+
input_items.append({"role": "assistant", "content": _responses_msg_content(content)})
|
|
2826
|
+
for tc in m.get("tool_calls") or []:
|
|
2827
|
+
input_items.append({
|
|
2828
|
+
"type": "function_call",
|
|
2829
|
+
"call_id": tc["id"],
|
|
2830
|
+
"name": tc["function"]["name"],
|
|
2831
|
+
"arguments": tc["function"]["arguments"],
|
|
2832
|
+
})
|
|
2833
|
+
elif role == "tool":
|
|
2834
|
+
input_items.append({
|
|
2835
|
+
"type": "function_call_output",
|
|
2836
|
+
"call_id": m.get("tool_call_id"),
|
|
2837
|
+
"output": m.get("content") or "",
|
|
2838
|
+
})
|
|
2839
|
+
else: # user
|
|
2840
|
+
input_items.append({"role": "user", "content": _responses_msg_content(content)})
|
|
2841
|
+
return instructions, input_items
|
|
2842
|
+
|
|
2843
|
+
|
|
2844
|
+
def _shim_message_from_responses(response) -> _ShimMessage:
|
|
2845
|
+
"""Map a Responses result's output items into a Chat-shaped message."""
|
|
2846
|
+
text_parts, tool_calls = [], []
|
|
2847
|
+
for item in (getattr(response, "output", None) or []):
|
|
2848
|
+
itype = getattr(item, "type", None)
|
|
2849
|
+
if itype == "function_call":
|
|
2850
|
+
tool_calls.append(_ShimToolCall(item.call_id, item.name, item.arguments))
|
|
2851
|
+
elif itype == "message":
|
|
2852
|
+
for c in (getattr(item, "content", None) or []):
|
|
2853
|
+
if getattr(c, "type", None) in ("output_text", "text"):
|
|
2854
|
+
text_parts.append(getattr(c, "text", "") or "")
|
|
2855
|
+
# reasoning items are not echoed back (state is rebuilt from history each turn)
|
|
2856
|
+
text = "".join(text_parts) or (getattr(response, "output_text", "") or "")
|
|
2857
|
+
return _ShimMessage(text, tool_calls)
|
|
2858
|
+
|
|
2859
|
+
|
|
2860
|
+
def _shim_usage_from_responses(usage):
|
|
2861
|
+
if usage is None:
|
|
2862
|
+
return None
|
|
2863
|
+
inp = int(getattr(usage, "input_tokens", 0) or 0)
|
|
2864
|
+
out = int(getattr(usage, "output_tokens", 0) or 0)
|
|
2865
|
+
details = getattr(usage, "input_tokens_details", None)
|
|
2866
|
+
cached = int(getattr(details, "cached_tokens", 0) or 0) if details else 0
|
|
2867
|
+
return _ShimUsage(inp, out, cached)
|
|
2868
|
+
|
|
2869
|
+
|
|
2870
|
+
def _orchestrator_turn(client, messages, tools, max_tokens):
|
|
2871
|
+
"""One orchestrator LLM turn. Returns (message, usage) shaped like Chat
|
|
2872
|
+
Completions, using either the Chat or Responses API per ERP_CHAT_API.
|
|
2873
|
+
Blocking — call via asyncio.to_thread."""
|
|
2874
|
+
if _use_responses_api():
|
|
2875
|
+
instructions, input_items = _responses_request_from_chat(messages)
|
|
2876
|
+
resp = client.responses.create(
|
|
2877
|
+
model=ORCHESTRATOR_MODEL,
|
|
2878
|
+
instructions=instructions,
|
|
2879
|
+
input=input_items,
|
|
2880
|
+
tools=_responses_tools_from_chat(tools),
|
|
2881
|
+
tool_choice="auto",
|
|
2882
|
+
max_output_tokens=max_tokens,
|
|
2883
|
+
reasoning={"effort": _RESPONSES_REASONING_EFFORT},
|
|
2884
|
+
)
|
|
2885
|
+
return _shim_message_from_responses(resp), _shim_usage_from_responses(getattr(resp, "usage", None))
|
|
2886
|
+
resp = client.chat.completions.create(
|
|
2887
|
+
model=ORCHESTRATOR_MODEL,
|
|
2888
|
+
messages=messages,
|
|
2889
|
+
tools=tools,
|
|
2890
|
+
tool_choice="auto",
|
|
2891
|
+
max_completion_tokens=max_tokens,
|
|
2892
|
+
# gpt-5.6-terra rejects function tools on /v1/chat/completions unless
|
|
2893
|
+
# reasoning_effort is "none" (400 otherwise).
|
|
2894
|
+
reasoning_effort="none",
|
|
2895
|
+
)
|
|
2896
|
+
return resp.choices[0].message, getattr(resp, "usage", None)
|
|
2897
|
+
|
|
2898
|
+
|
|
2701
2899
|
async def run_thinking_loop(
|
|
2702
2900
|
messages: list[dict],
|
|
2703
2901
|
on_event,
|
|
@@ -2825,24 +3023,16 @@ async def run_thinking_loop(
|
|
|
2825
3023
|
print(f"[chat_llm] provider=openai model={model_name} session_id={session_id or '-'} iter={iteration + 1}", flush=True)
|
|
2826
3024
|
await on_event({"type": "llm_provider", "provider": "openai", "model": model_name})
|
|
2827
3025
|
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",
|
|
3026
|
+
# One orchestrator turn via Chat Completions (default) or the
|
|
3027
|
+
# Responses API (ERP_CHAT_API=responses). Returns Chat-shaped
|
|
3028
|
+
# (message, usage) either way, so everything below is unchanged.
|
|
3029
|
+
message, usage = await asyncio.to_thread(
|
|
3030
|
+
_orchestrator_turn, openai_client, messages, build_tools(), max_completion,
|
|
2840
3031
|
)
|
|
2841
3032
|
except Exception as e:
|
|
2842
3033
|
await on_event({"type": "error", "content": f"Error calling LLM: {e}"})
|
|
2843
3034
|
return
|
|
2844
3035
|
|
|
2845
|
-
usage = getattr(response, "usage", None)
|
|
2846
3036
|
demo_limiter.settle(
|
|
2847
3037
|
reservation_id,
|
|
2848
3038
|
actual_cost_usd=cost_of_openai_call(model_name, usage),
|
|
@@ -2859,9 +3049,6 @@ async def run_thinking_loop(
|
|
|
2859
3049
|
if not settled:
|
|
2860
3050
|
demo_limiter.release(reservation_id)
|
|
2861
3051
|
|
|
2862
|
-
choice = response.choices[0]
|
|
2863
|
-
message = choice.message
|
|
2864
|
-
|
|
2865
3052
|
if not message.tool_calls:
|
|
2866
3053
|
content = message.content or ""
|
|
2867
3054
|
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
|