Repository navigation
test(api): lock chat/Completions service_tier honesty over HTTP #416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -222,14 +222,21 @@ class UsageRecord: | |
|
|
||
| def as_dict(self) -> Dict[str, Any]: | ||
| """Flatten the record (attribution inlined) for JSON + SQL storage.""" | ||
| # Prefer an explicit attribution model_name (client tag) for rollups; | ||
| # otherwise the served model id on the record. | ||
| rollup_model = ( | ||
| self.attribution.model_name | ||
| if self.attribution.model_name != UNATTRIBUTED | ||
| else self.model_name | ||
| ) | ||
| row = { | ||
| "usage_record_id": self.usage_record_id, | ||
| "created_at": self.created_at, | ||
| "workflow_run_id": self.workflow_run_id, | ||
| "request_channel": self.request_channel, | ||
| "route_mode": self.route_mode, | ||
| "provider_name": self.provider_name, | ||
| "model_name": self.model_name, | ||
| "model_name": rollup_model, | ||
| "prompt_tokens": self.prompt_tokens, | ||
| "completion_tokens": self.completion_tokens, | ||
| "total_tokens": self.total_tokens, | ||
|
|
@@ -583,12 +590,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. | ||
|
Comment on lines
+593
to
+598
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target lines ---'
nl -ba contextual_orchestrator/cost_ledger.py | sed -n '575,645p'
printf '%s\n' '--- ruff configuration references ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
'(^|[^A-Za-z])(ruff|S608|flake8-bandit|select|extend-select|ignore|extend-ignore|noqa)([^A-Za-z]|$)' \
pyproject.toml setup.cfg tox.ini .ruff.toml ruff.toml 2>/dev/null || true
printf '%s\n' '--- ruff availability ---'
if command -v ruff >/dev/null 2>&1; then ruff --version; else echo 'ruff: unavailable'; fiRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 581 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- target lines ---'
awk 'NR>=575 && NR<=645 {printf "%6d\t%s\n", NR, $0}' contextual_orchestrator/cost_ledger.py
printf '%s\n' '--- Ruff configuration ---'
awk 'NR>=1 && NR<=180 {printf "%6d\t%s\n", NR, $0}' pyproject.toml
printf '%s\n' '--- S608 diagnostics ---'
ruff check --select S608 contextual_orchestrator/cost_ledger.pyRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 9132 🏁 Script executed: #!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import subprocess
path = Path("contextual_orchestrator/cost_ledger.py")
source = path.read_text()
def check(label, replacements):
text = source
for old, new in replacements:
if old not in text:
raise SystemExit(f"missing replacement for {label}: {old!r}")
text = text.replace(old, new, 1)
result = subprocess.run(
["ruff", "check", "--select", "S608", "--stdin-filename", str(path), "-"],
input=text,
text=True,
capture_output=True,
)
print(f"--- {label}: exit={result.returncode} ---")
print(result.stdout or "(no stdout)")
if result.stderr:
print(result.stderr)
check("only proposed lines 594 and 632", [
(
'f"SELECT 1 FROM cost_attribution_dimensions WHERE dimension_name = {ph}", # nosec',
'f"SELECT 1 FROM cost_attribution_dimensions WHERE dimension_name = {ph}", # noqa: S608 # nosec',
),
(
'cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # nosec',
'cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # noqa: S608 # nosec',
),
])
check("all four diagnostic lines", [
(
'f"SELECT 1 FROM cost_attribution_dimensions WHERE dimension_name = {ph}", # nosec',
'f"SELECT 1 FROM cost_attribution_dimensions WHERE dimension_name = {ph}", # noqa: S608 # nosec',
),
(
'"INSERT INTO cost_attribution_dimensions "',
'"INSERT INTO cost_attribution_dimensions " # noqa: S608',
),
(
'f"INSERT INTO llm_usage_records ({columns}) VALUES ({placeholders})", # nosec',
'f"INSERT INTO llm_usage_records ({columns}) VALUES ({placeholders})", # noqa: S608 # nosec',
),
(
'cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # nosec',
'cur.execute(f"SELECT {columns} FROM llm_usage_records{where}", tuple(params)) # noqa: S608 # nosec',
),
])
PYRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 2236 모든 Ruff S608 진단 위치에 억제를 추가하세요. Ruff 0.16.1은 594, 599, 613, 632행에서 S608을 보고합니다. 각 진단 기준 행에 🧰 Tools🪛 Ruff (0.16.1)[error] 594-594: Possible SQL injection vector through string-based query construction (S608) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| "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), | ||
|
|
@@ -602,7 +609,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), | ||
| ) | ||
|
|
@@ -622,7 +629,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()] | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -215,6 +215,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 | ||||||||||||||
|
Comment on lines
+218
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# 샘플링 옵션의 검증, 호출, provider payload 전달 경로를 확인합니다.
rg -n -C 5 'presence_penalty|frequency_penalty|top_p|stream_chat|batch_chat|client\.chat|client\.stream_chat|client\.batch_chat' contextual_orchestrator testsRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 37437 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ModelClient method definitions ---'
ast-grep outline contextual_orchestrator/orchestrator.py --match 'class ModelClient' --view expanded
printf '%s\n' '--- chat/stream_chat/batch_chat implementations ---'
sed -n '245,430p' contextual_orchestrator/orchestrator.py
sed -n '533,620p' contextual_orchestrator/orchestrator.py
printf '%s\n' '--- stream and batch call sites ---'
sed -n '950,1010p' contextual_orchestrator/orchestrator.py
sed -n '1070,1110p' contextual_orchestrator/orchestrator.py
printf '%s\n' '--- server option application and route dispatch ---'
sed -n '2098,2142p' contextual_orchestrator/server.py
sed -n '2360,2420p' contextual_orchestrator/server.pyRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 26472 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import ast
from pathlib import Path
path = Path("contextual_orchestrator/orchestrator.py")
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == "ModelClient":
for item in node.body:
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and item.name in {
"chat", "stream_chat", "batch_chat"
}:
args = [arg.arg for arg in item.args.args]
print(f"{item.name} signature: {args}")
keys = []
for child in ast.walk(item):
if isinstance(child, ast.Dict):
for key in child.keys:
if isinstance(key, ast.Constant) and key.value in {
"temperature", "top_p",
"presence_penalty", "frequency_penalty",
}:
keys.append(key.value)
print(f"{item.name} payload keys: {sorted(set(keys))}")
PYRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 524 모든 completion 경로에 요청별 샘플링 옵션을 전달하세요.
🤖 Prompt for AI Agents |
||||||||||||||
| self.max_retries = max_retries | ||||||||||||||
| self.retry_backoff = retry_backoff | ||||||||||||||
| self.retry_backoff_cap = retry_backoff_cap | ||||||||||||||
|
|
@@ -230,7 +234,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. | ||||||||||||||
|
Comment on lines
235
to
+237
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# TLS 검증 해제 설정과 ModelClient 생성부를 확인합니다.
rg -n -C 6 'ModelClient\(|verify_tls|create_unverified_context|CONTEXTUAL_ORCHESTRATOR_' contextual_orchestrator testsRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 37271 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ModelClient and TLS implementation ---'
sed -n '190,275p' contextual_orchestrator/orchestrator.py
printf '%s\n' '--- CLI construction and serving path ---'
sed -n '60,145p' contextual_orchestrator/__main__.py
printf '%s\n' '--- all insecure TLS references ---'
rg -n -C 4 'insecure[-_]skip[-_]tls|verify_tls\s*=|ModelClient\(' --glob '*.py' --glob '*.md' --glob '*.yml' --glob '*.yaml' .
printf '%s\n' '--- production/runtime mode indicators ---'
rg -n -i 'production|prod|dev[-_ ]only|development|environment|ENVIRONMENT|serve\(' --glob '*.py' --glob '*.md' --glob '*.yml' --glob '*.yaml' contextual_orchestrator tests README.md pyproject.toml 2>/dev/null || trueRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 50399 🏁 Script executed: python3 - <<'PY'
import ast
from pathlib import Path
files = [
Path("contextual_orchestrator/orchestrator.py"),
Path("contextual_orchestrator/__main__.py"),
]
trees = {p: ast.parse(p.read_text(), filename=str(p)) for p in files}
orchestrator = trees[files[0]]
model_client = next(
n for n in ast.walk(orchestrator)
if isinstance(n, ast.ClassDef) and n.name == "ModelClient"
)
init = next(
n for n in model_client.body
if isinstance(n, ast.FunctionDef) and n.name == "__init__"
)
params = {arg.arg: arg for arg in init.args.args}
defaults = dict(zip((a.arg for a in init.args.args[-len(init.args.defaults):]), init.args.defaults))
print("ModelClient.verify_tls_default =", ast.unparse(defaults["verify_tls"]))
print("ModelClient._build_ssl_context_calls_unverified =",
any(isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute)
and n.func.attr == "_create_unverified_context"
for n in ast.walk(model_client)))
main = trees[files[1]]
flag = next(
n for n in ast.walk(main)
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Attribute)
and n.func.attr == "add_argument"
and n.args
and isinstance(n.args[0], ast.Constant)
and n.args[0].value == "--insecure-skip-tls-verify"
)
client_call = next(
n for n in ast.walk(main)
if isinstance(n, ast.Call)
and isinstance(n.func, ast.Name)
and n.func.id == "ModelClient"
)
print("CLI_insecure_flag_present =", ast.unparse(flag.args[0]))
print("CLI_passes_verify_tls =", any(
isinstance(k, ast.keyword) and k.arg == "verify_tls"
for k in client_call.keywords
))
print("CLI_has_environment_guard_for_insecure_flag =", any(
isinstance(n, (ast.If, ast.Assert, ast.Raise))
and "insecure" in ast.unparse(n).lower()
for n in ast.walk(main)
))
PYRepository: ContextualWisdomLab/contextual-orchestrator Length of output: 412 운영 환경에서 TLS 검증 해제를 차단하세요.
운영 호출부에서 🧰 Tools🪛 ast-grep (0.45.1)[info] 236-236: should not bypass certificate verification (ssl-unverified-context) 🪛 Ruff (0.16.1)[error] 237-237: Python allows using an insecure context via the (S323) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||
| if ca_bundle: | ||||||||||||||
| if not os.path.isfile(ca_bundle): | ||||||||||||||
| raise ValueError(f"provider CA bundle does not exist: {ca_bundle}") | ||||||||||||||
|
|
@@ -246,9 +250,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) | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -262,10 +286,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: | ||||||||||||||
|
|
@@ -307,7 +337,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, | ||||||||||||||
|
|
@@ -897,7 +927,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() | ||||||||||||||
|
|
@@ -8517,6 +8563,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()), | ||||||||||||||
|
Comment on lines
+8573
to
+8575
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 레거시 Completions 응답 ID의 고유성을 보장하세요. Line [8573]은 밀리초 단위 시각만 사용합니다. 동시 요청이 같은 밀리초에 완료되면 두 응답이 같은 이 파일에서 이미 사용하는 권장 수정- "id": f"cmpl-{int(time.time() * 1000)}",
+ "id": f"cmpl-{uuid.uuid4().hex}",📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| "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 | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: ContextualWisdomLab/contextual-orchestrator
Length of output: 25322
🏁 Script executed:
Repository: ContextualWisdomLab/contextual-orchestrator
Length of output: 50400
🏁 Script executed:
Repository: ContextualWisdomLab/contextual-orchestrator
Length of output: 4294
🏁 Script executed:
Repository: ContextualWisdomLab/contextual-orchestrator
Length of output: 50400
model_name을 제공 모델 ID로 유지하세요.UsageRecord.model_name과UsageTelemetryEvent는 제공 모델 ID를 사용합니다. 그러나UsageRecord.as_dict()는 명시적 attributionmodel_name으로 SQL 저장값과/api/v1/llm_usage_records응답을 덮어씁니다. 따라서 동일 레코드가 API·SQL과 telemetry에서 서로 다른 모델명을 보고합니다. attribution 모델명은 별도 롤업 필드로 저장하고 관련 스키마와 회귀 테스트를 추가하세요.🤖 Prompt for AI Agents