Verify native OCI web search across seven Grok model IDs

This commit is contained in:
Marcos Lohmann
2026-10-14 11:35:30 -03:00
parent 53ebadd8de
commit 7f2ce6750b
9 changed files with 587 additions and 4 deletions

View File

@@ -1,4 +1,5 @@
OCI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1 OCI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1
OCI_MODEL=xai.grok-4.3
OCI_API_KEY=replace-with-oci-generative-ai-api-key OCI_API_KEY=replace-with-oci-generative-ai-api-key
OCI_PROJECT_ID=replace-with-active-chicago-project-id OCI_PROJECT_ID=replace-with-active-chicago-project-id
APP_PASSWORD=replace-with-a-strong-password APP_PASSWORD=replace-with-a-strong-password

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ render_check.py
evidence/* evidence/*
!evidence/RELATORIO.md !evidence/RELATORIO.md
!evidence/validation-summary.json !evidence/validation-summary.json
!evidence/grok-websearch-models.json

View File

@@ -1,6 +1,6 @@
# Grok Web Search via OCI # Grok Web Search via OCI
POC de chatbot com interface JavaScript e backend Python/FastAPI. Usa `xai.grok-4.3` pela OCI Generative AI e habilita a ferramenta nativa `web_search` na Responses API. A aplicação chama exclusivamente o endpoint OCI. POC de chatbot com interface JavaScript e backend Python/FastAPI. Usa Grok (padrão `xai.grok-4.3`, configurável por `OCI_MODEL`) pela OCI Generative AI e habilita a ferramenta nativa `web_search` na Responses API. A aplicação chama exclusivamente o endpoint OCI.
## Estrutura ## Estrutura
@@ -11,14 +11,17 @@ POC de chatbot com interface JavaScript e backend Python/FastAPI. Usa `xai.grok-
│ ├── index.html # Interface e estilos │ ├── index.html # Interface e estilos
│ └── app.js # Chat, histórico e citações │ └── app.js # Chat, histórico e citações
├── requirements.txt ├── requirements.txt
├── requirements-test.txt # TestClient para matriz de modelos
├── Dockerfile ├── Dockerfile
├── .dockerignore ├── .dockerignore
├── .gitignore ├── .gitignore
├── .env.example # Variáveis sem credenciais ├── .env.example # Variáveis sem credenciais
├── validate.py # Validação REST com inferência real ├── validate.py # Validação REST com inferência real
├── test_models.py # Matriz de modelos pela mesma aplicação
└── evidence/ └── evidence/
├── RELATORIO.md # Resultados da POC ├── RELATORIO.md # Resultados da POC
└── validation-summary.json # Evidências selecionadas ├── validation-summary.json # Evidências selecionadas
└── grok-websearch-models.json # Matriz de compatibilidade datada
``` ```
## Configuração ## Configuração
@@ -37,6 +40,7 @@ Preencha `.env`:
| Variável | Finalidade | | Variável | Finalidade |
|---|---| |---|---|
| `OCI_BASE_URL` | `https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1` | | `OCI_BASE_URL` | `https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/v1` |
| `OCI_MODEL` | Modelo Grok; padrão `xai.grok-4.3` |
| `OCI_API_KEY` | Chave OCI Generative AI | | `OCI_API_KEY` | Chave OCI Generative AI |
| `OCI_PROJECT_ID` | Identificador de projeto ativo em Chicago | | `OCI_PROJECT_ID` | Identificador de projeto ativo em Chicago |
| `APP_PASSWORD` | Senha forte para o usuário `poc` | | `APP_PASSWORD` | Senha forte para o usuário `poc` |
@@ -92,6 +96,49 @@ A Oracle informa que os modelos Grok acessados pela OCI são hospedados externam
Nos testes de 14/10/2026, `/openai/v1/responses` rejeitou `web_search` para Grok; `/v1/responses` e `/20231130/actions/v1/responses` funcionaram com um projeto ativo de Chicago. A aplicação usa `/v1/responses`. Nos testes de 14/10/2026, `/openai/v1/responses` rejeitou `web_search` para Grok; `/v1/responses` e `/20231130/actions/v1/responses` funcionaram com um projeto ativo de Chicago. A aplicação usa `/v1/responses`.
## Modelos com web_search confirmado em 14/10/2026
Teste da mesma rota `/api/chat` em processos locais isolados via FastAPI TestClient, com inferência real na OCI **us-chicago-1**, endpoint **`/v1/responses`**, projeto ativo e `tools: [{"type":"web_search"}]`. Não foram chamadas APIs diretas da xAI. A implantação pública original foi validada com Grok 4.3; esta matriz usa a aplicação local para variar os modelos sem interromper a VM.
| Modelo solicitado e retornado | Busca nativa | HTTP app/OCI | Buscas concluídas | Citações | Tempo (s) |
|---|---|---|---:|---:|---:|
| `xai.grok-4.20-0309-non-reasoning` | Confirmado | 200/200 | 2 | 1 | 3.62 |
| `xai.grok-4.20-0309-reasoning` | Confirmado | 200/200 | 1 | 2 | 14.80 |
| `xai.grok-4.20-non-reasoning` | Confirmado | 200/200 | 2 | 1 | 3.22 |
| `xai.grok-4.20-reasoning` | Confirmado | 200/200 | 2 | 1 | 17.09 |
| `xai.grok-4.3` | Confirmado | 200/200 | 2 | 1 | 9.24 |
| `xai.grok-4.6` | Confirmado | 200/200 | 1 | 1 | 35.17 |
| `xai.grok-4.7` | Confirmado | 200/200 | 3 | 1 | 12.97 |
Critério de confirmação: HTTP 200, pelo menos um evento `web_search_call` com ação `search` e status `completed`, fontes não vazias e citações estruturadas. HTTP 200 isolado não comprova a execução de busca.
Foram sete chamadas, sem retries, com até duas inferências simultâneas e 2400 tokens de saída por chamada. Todos os modelos retornaram exatamente o identificador solicitado. Os testes cobrem os sete modelos de texto Grok do catálogo local; Voice Agent, Multi-Agent e outros identificadores não foram testados. O resultado vale para esta data, região, endpoint e permissões; não confirma todas as regiões ou interfaces OCI. A validação da busca não equivale à verificação independente de todas as notícias retornadas.
Os tempos são observações de uma chamada por modelo, sob concorrência limitada, e não constituem benchmark. Consumo agregado: **76.374 tokens** (69.948 de entrada e 6.426 de saída), com **13 ferramentas** contabilizadas pelo serviço. `cost_in_usd_ticks` foi preservado sem conversão financeira.
Para escolher um modelo, defina `OCI_MODEL` no `.env` e reinicie o serviço. O padrão continua `xai.grok-4.3`; `/health` e o indicador da UI refletem a configuração.
Para reproduzir a matriz (sete chamadas pagas):
```bash
. .venv/bin/activate
pip install -r requirements-test.txt
set -a
. ./.env
set +a
python test_models.py
```
Para testar somente um modelo:
```bash
python test_models.py --models xai.grok-4.7
```
O [script](test_models.py) salva respostas brutas localmente em `./evidence/model-tests-raw/` (ignoradas pelo Git) e um [resumo da matriz](evidence/grok-websearch-models.json), sem credenciais nem identificadores de projeto. Execuções parciais na mesma data preservam os resultados dos demais modelos já testados. As versões do runtime usado na matriz original estão no JSON; a reprodução instala as dependências fixadas do aplicativo.
Referências do fornecedor: [Grok 4.20](https://docs.oracle.com/en-us/iaas/Content/generative-ai/xai-grok-4-20.htm), [Grok 4.6](https://docs.oracle.com/en-us/iaas/Content/generative-ai/xai-grok-4-6.htm) e [Grok 4.7](https://docs.oracle.com/en-us/iaas/Content/generative-ai/x-ai-grok-4-7.htm). A confirmação nesta tabela vem dos testes de execução.
## API REST ## API REST
`POST /api/chat` com HTTP Basic. Na publicação com prefixo, use `POST /grok-poc/api/chat`. `POST /api/chat` com HTTP Basic. Na publicação com prefixo, use `POST /grok-poc/api/chat`.

4
app.py
View File

@@ -20,7 +20,7 @@ class Chat(BaseModel):
message:str=Field(min_length=1,max_length=8000) message:str=Field(min_length=1,max_length=8000)
history:list[Turn]=Field(default_factory=list,max_length=12) history:list[Turn]=Field(default_factory=list,max_length=12)
@app.get('/health') @app.get('/health')
def health():return {'status':'ok','provider':'OCI','model':'xai.grok-4.3','tool':'web_search'} def health():return {'status':'ok','provider':'OCI','model':os.environ.get('OCI_MODEL','xai.grok-4.3'),'tool':'web_search'}
@app.get('/',dependencies=[Depends(authorize)]) @app.get('/',dependencies=[Depends(authorize)])
def index():return FileResponse('static/index.html') def index():return FileResponse('static/index.html')
@app.get('/app.js',dependencies=[Depends(authorize)]) @app.get('/app.js',dependencies=[Depends(authorize)])
@@ -32,7 +32,7 @@ async def chat(q:Chat):
base=os.environ['OCI_BASE_URL'].rstrip('/') base=os.environ['OCI_BASE_URL'].rstrip('/')
if urlparse(base).hostname!='inference.generativeai.us-chicago-1.oci.oraclecloud.com':raise HTTPException(503,'Endpoint OCI inválido') if urlparse(base).hostname!='inference.generativeai.us-chicago-1.oci.oraclecloud.com':raise HTTPException(503,'Endpoint OCI inválido')
now=datetime.now(ZoneInfo('America/Sao_Paulo')) now=datetime.now(ZoneInfo('America/Sao_Paulo'))
body={'model':'xai.grok-4.3','input':[t.model_dump() for t in q.history]+[{'role':'user','content':q.message}], body={'model':os.environ.get('OCI_MODEL','xai.grok-4.3'),'input':[t.model_dump() for t in q.history]+[{'role':'user','content':q.message}],
'instructions':f'Você é um assistente em português brasileiro. Hoje é {now.isoformat()}. Use a ferramenta web_search para verificar informações atuais. Cite fontes com URLs e datas. Diferencie data de publicação e data do fato. Nunca invente notícias; se não houver evidência de hoje, diga isso claramente.', 'instructions':f'Você é um assistente em português brasileiro. Hoje é {now.isoformat()}. Use a ferramenta web_search para verificar informações atuais. Cite fontes com URLs e datas. Diferencie data de publicação e data do fato. Nunca invente notícias; se não houver evidência de hoje, diga isso claramente.',
'tools':[{'type':'web_search'}],'store':False,'max_output_tokens':2400} 'tools':[{'type':'web_search'}],'store':False,'max_output_tokens':2400}
started=time.monotonic() started=time.monotonic()

View File

@@ -19,3 +19,7 @@ Fonte: [Agência Brasil — Confira a agenda dos presidenciáveis nesta quarta-f
A ferramenta executou uma busca inicial que falhou, reformulou consultas e concluiu a pesquisa. As respostas brutas, identificadores OCI, capturas e cópia integral da fonte permanecem locais e não são versionados. O SHA-256 da fonte permite conferir o artefato local. A ferramenta executou uma busca inicial que falhou, reformulou consultas e concluiu a pesquisa. As respostas brutas, identificadores OCI, capturas e cópia integral da fonte permanecem locais e não são versionados. O SHA-256 da fonte permite conferir o artefato local.
A chamada final consumiu 15.544 tokens. Incluindo os diagnósticos bem-sucedidos, foram 34.829 tokens e nove ferramentas contabilizadas pelo serviço. A escala financeira de cost_in_usd_ticks não foi verificada; o campo é preservado sem conversão para dólares. A chamada final consumiu 15.544 tokens. Incluindo os diagnósticos bem-sucedidos, foram 34.829 tokens e nove ferramentas contabilizadas pelo serviço. A escala financeira de cost_in_usd_ticks não foi verificada; o campo é preservado sem conversão para dólares.
## Matriz de versões Grok
Em 14/10/2026, sete modelos de texto passaram no teste da mesma aplicação via TestClient e inferência OCI real: Grok 4.3, 4.6, 4.7 e as quatro variantes 4.20. Veja a [matriz estruturada](grok-websearch-models.json) e a [metodologia no README](../README.md#modelos-com-web_search-confirmado-em-14102026). Cada confirmação exige busca concluída com fontes e citações; os tempos são observações isoladas. A matriz consumiu 76.374 tokens e 13 ferramentas contabilizadas pelo serviço, separadamente das validações originais acima.

View File

@@ -0,0 +1,414 @@
{
"tested_at": "2026-10-14T11:27:10.525049-03:00",
"region": "us-chicago-1",
"endpoint": "/v1/responses",
"method": "POST /api/chat via FastAPI TestClient; inferência OCI real; um processo isolado por modelo",
"max_output_tokens": 2400,
"concurrency": 2,
"retries": 0,
"confirmation_criteria": "HTTP 200, search completed, fontes não vazias e citações estruturadas",
"results": [
{
"requested_model": "xai.grok-4.20-0309-non-reasoning",
"returned_model": "xai.grok-4.20-0309-non-reasoning",
"tested_at": "2026-10-14T11:26:10.793751-03:00",
"app_http": 200,
"oci_http": 200,
"seconds": 3.62,
"websearch_confirmed": true,
"completed_searches": 2,
"source_count": 15,
"citation_count": 1,
"tool_events": [
{
"status": "completed",
"action": "search"
},
{
"status": "completed",
"action": "search"
}
],
"usage": {
"input_tokens": 10036,
"input_tokens_details": {
"cached_tokens": 1024
},
"output_tokens": 196,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 10232,
"num_sources_used": 0,
"num_server_side_tools_used": 2,
"cost_in_usd_ticks": 219598000,
"server_side_tool_usage_details": {
"web_search_calls": 2,
"x_search_calls": 0,
"x_posts_fetched": 0,
"x_users_fetched": 0,
"code_interpreter_calls": 0,
"file_search_calls": 0,
"mcp_calls": 0,
"document_search_calls": 0,
"image_generation_calls": 0
},
"context_details": {
"input_tokens": 9058,
"output_tokens": 138
}
},
"error": null,
"health_model": "xai.grok-4.20-0309-non-reasoning",
"citation_urls": [
"https://ndmais.com.br/tempo/tempestades-1-342-municipios-alerta-laranja-quarta-feira-14/"
]
},
{
"requested_model": "xai.grok-4.20-0309-reasoning",
"returned_model": "xai.grok-4.20-0309-reasoning",
"tested_at": "2026-10-14T11:26:10.782598-03:00",
"app_http": 200,
"oci_http": 200,
"seconds": 14.8,
"websearch_confirmed": true,
"completed_searches": 1,
"source_count": 10,
"citation_count": 2,
"tool_events": [
{
"status": "completed",
"action": "search"
}
],
"usage": {
"input_tokens": 7712,
"input_tokens_details": {
"cached_tokens": 256
},
"output_tokens": 1491,
"output_tokens_details": {
"reasoning_tokens": 1375
},
"total_tokens": 9203,
"num_sources_used": 0,
"num_server_side_tools_used": 1,
"cost_in_usd_ticks": 180987000,
"server_side_tool_usage_details": {
"web_search_calls": 1,
"x_search_calls": 0,
"x_posts_fetched": 0,
"x_users_fetched": 0,
"code_interpreter_calls": 0,
"file_search_calls": 0,
"mcp_calls": 0,
"document_search_calls": 0,
"image_generation_calls": 0
},
"context_details": {
"input_tokens": 6732,
"output_tokens": 864
}
},
"error": null,
"health_model": "xai.grok-4.20-0309-reasoning",
"citation_urls": [
"https://correiodobrasil.com.br/c/ultimas-noticias/"
]
},
{
"requested_model": "xai.grok-4.20-non-reasoning",
"returned_model": "xai.grok-4.20-non-reasoning",
"tested_at": "2026-10-14T11:26:14.931513-03:00",
"app_http": 200,
"oci_http": 200,
"seconds": 3.22,
"websearch_confirmed": true,
"completed_searches": 2,
"source_count": 15,
"citation_count": 1,
"tool_events": [
{
"status": "completed",
"action": "search"
},
{
"status": "completed",
"action": "search"
}
],
"usage": {
"input_tokens": 10944,
"input_tokens_details": {
"cached_tokens": 1024
},
"output_tokens": 224,
"output_tokens_details": {
"reasoning_tokens": 0
},
"total_tokens": 11168,
"num_sources_used": 0,
"num_server_side_tools_used": 2,
"cost_in_usd_ticks": 231648000,
"server_side_tool_usage_details": {
"web_search_calls": 2,
"x_search_calls": 0,
"x_posts_fetched": 0,
"x_users_fetched": 0,
"code_interpreter_calls": 0,
"file_search_calls": 0,
"mcp_calls": 0,
"document_search_calls": 0,
"image_generation_calls": 0
},
"context_details": {
"input_tokens": 9966,
"output_tokens": 153
}
},
"error": null,
"health_model": "xai.grok-4.20-non-reasoning",
"citation_urls": [
"https://ndmais.com.br/tempo/tempestades-1-342-municipios-alerta-laranja-quarta-feira-14/"
]
},
{
"requested_model": "xai.grok-4.20-reasoning",
"returned_model": "xai.grok-4.20-reasoning",
"tested_at": "2026-10-14T11:26:18.687154-03:00",
"app_http": 200,
"oci_http": 200,
"seconds": 17.09,
"websearch_confirmed": true,
"completed_searches": 2,
"source_count": 10,
"citation_count": 1,
"tool_events": [
{
"status": "completed",
"action": "search"
},
{
"status": "completed",
"action": "search"
}
],
"usage": {
"input_tokens": 7882,
"input_tokens_details": {
"cached_tokens": 1024
},
"output_tokens": 1531,
"output_tokens_details": {
"reasoning_tokens": 1329
},
"total_tokens": 9413,
"num_sources_used": 0,
"num_server_side_tools_used": 2,
"cost_in_usd_ticks": 226048000,
"server_side_tool_usage_details": {
"web_search_calls": 2,
"x_search_calls": 0,
"x_posts_fetched": 0,
"x_users_fetched": 0,
"code_interpreter_calls": 0,
"file_search_calls": 0,
"mcp_calls": 0,
"document_search_calls": 0,
"image_generation_calls": 0
},
"context_details": {
"input_tokens": 6902,
"output_tokens": 732
}
},
"error": null,
"health_model": "xai.grok-4.20-reasoning",
"citation_urls": [
"https://paraibaonline.com.br/brasil/2026/10/14/o-que-tem-nas-capas-dos-jornais-do-brasil-nesta-quarta-feira-14-10/"
]
},
{
"requested_model": "xai.grok-4.3",
"returned_model": "xai.grok-4.3",
"tested_at": "2026-10-14T11:26:26.105038-03:00",
"app_http": 200,
"oci_http": 200,
"seconds": 9.24,
"websearch_confirmed": true,
"completed_searches": 2,
"source_count": 10,
"citation_count": 1,
"tool_events": [
{
"status": "completed",
"action": "search"
},
{
"status": "completed",
"action": "search"
}
],
"usage": {
"input_tokens": 8092,
"input_tokens_details": {
"cached_tokens": 1280
},
"output_tokens": 898,
"output_tokens_details": {
"reasoning_tokens": 734
},
"total_tokens": 8990,
"num_sources_used": 0,
"num_server_side_tools_used": 2,
"cost_in_usd_ticks": 210160000,
"server_side_tool_usage_details": {
"web_search_calls": 2,
"x_search_calls": 0,
"x_posts_fetched": 0,
"x_users_fetched": 0,
"code_interpreter_calls": 0,
"file_search_calls": 0,
"mcp_calls": 0,
"document_search_calls": 0,
"image_generation_calls": 0
},
"context_details": {
"input_tokens": 6926,
"output_tokens": 456
}
},
"error": null,
"health_model": "xai.grok-4.3",
"citation_urls": [
"https://www.cnnbrasil.com.br/nacional/brasil/temporais-e-chuva-forte-veja-como-fica-o-tempo-no-brasil-nesta-quarta-14/"
]
},
{
"requested_model": "xai.grok-4.6",
"returned_model": "xai.grok-4.6",
"tested_at": "2026-10-14T11:26:35.843431-03:00",
"app_http": 200,
"oci_http": 200,
"seconds": 35.17,
"websearch_confirmed": true,
"completed_searches": 1,
"source_count": 10,
"citation_count": 1,
"tool_events": [
{
"status": "completed",
"action": "search"
}
],
"usage": {
"input_tokens": 8712,
"input_tokens_details": {
"cached_tokens": 1280
},
"output_tokens": 878,
"output_tokens_details": {
"reasoning_tokens": 732
},
"total_tokens": 9590,
"num_sources_used": 0,
"num_server_side_tools_used": 1,
"cost_in_usd_ticks": 257720000,
"server_side_tool_usage_details": {
"web_search_calls": 1,
"x_search_calls": 0,
"x_posts_fetched": 0,
"x_users_fetched": 0,
"code_interpreter_calls": 0,
"file_search_calls": 0,
"mcp_calls": 0,
"document_search_calls": 0,
"image_generation_calls": 0
},
"context_details": {
"input_tokens": 7510,
"output_tokens": 535
}
},
"error": null,
"health_model": "xai.grok-4.6",
"citation_urls": [
"https://www.cnnbrasil.com.br/nacional/brasil/temporais-e-chuva-forte-veja-como-fica-o-tempo-no-brasil-nesta-quarta-14/"
]
},
{
"requested_model": "xai.grok-4.7",
"returned_model": "xai.grok-4.7",
"tested_at": "2026-10-14T11:24:43.909978-03:00",
"app_http": 200,
"oci_http": 200,
"seconds": 12.97,
"websearch_confirmed": true,
"completed_searches": 3,
"source_count": 20,
"citation_count": 1,
"tool_events": [
{
"status": "completed",
"action": "search"
},
{
"status": "completed",
"action": "search"
},
{
"status": "completed",
"action": "search"
}
],
"usage": {
"input_tokens": 16570,
"input_tokens_details": {
"cached_tokens": 3456
},
"output_tokens": 1208,
"output_tokens_details": {
"reasoning_tokens": 988
},
"total_tokens": 17778,
"num_sources_used": 0,
"num_server_side_tools_used": 3,
"cost_in_usd_ticks": 502040000,
"server_side_tool_usage_details": {
"web_search_calls": 3,
"x_search_calls": 0,
"x_posts_fetched": 0,
"x_users_fetched": 0,
"code_interpreter_calls": 0,
"file_search_calls": 0,
"mcp_calls": 0,
"document_search_calls": 0,
"image_generation_calls": 0
},
"context_details": {
"input_tokens": 14183,
"output_tokens": 1022
}
},
"error": null,
"health_model": "xai.grok-4.7",
"citation_urls": [
"https://ndmais.com.br/tempo/tempestades-1-342-municipios-alerta-laranja-quarta-feira-14/"
]
}
],
"aggregate_usage": {
"input_tokens": 69948,
"output_tokens": 6426,
"total_tokens": 76374,
"num_server_side_tools_used": 13,
"cost_in_usd_ticks": 1828201000
},
"test_runtime": {
"python": "3.12.7",
"fastapi": "0.143.1",
"starlette": "1.8.0",
"httpx": "0.28.1",
"requests": "2.34.2"
}
}

2
requirements-test.txt Normal file
View File

@@ -0,0 +1,2 @@
-r requirements.txt
httpx==0.28.1

View File

@@ -2,3 +2,5 @@ const form=document.querySelector('#form'),box=document.querySelector('#messages
function formatAnswer(div,text){div.textContent='';const pattern=/\*\*([^*]+)\*\*|\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g;let last=0;for(const m of text.matchAll(pattern)){div.append(document.createTextNode(text.slice(last,m.index)));const el=document.createElement(m[1]?'strong':'a');el.textContent=m[1]||m[2];if(!m[1]){el.href=m[3];el.target='_blank';el.rel='noopener noreferrer';}div.append(el);last=m.index+m[0].length;}div.append(document.createTextNode(text.slice(last)));} function formatAnswer(div,text){div.textContent='';const pattern=/\*\*([^*]+)\*\*|\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g;let last=0;for(const m of text.matchAll(pattern)){div.append(document.createTextNode(text.slice(last,m.index)));const el=document.createElement(m[1]?'strong':'a');el.textContent=m[1]||m[2];if(!m[1]){el.href=m[3];el.target='_blank';el.rel='noopener noreferrer';}div.append(el);last=m.index+m[0].length;}div.append(document.createTextNode(text.slice(last)));}
function turn(text,role){const div=document.createElement('div');div.className='turn '+role;div.textContent=text;box.append(div);return div;} function turn(text,role){const div=document.createElement('div');div.className='turn '+role;div.textContent=text;box.append(div);return div;}
form.addEventListener('submit',async e=>{e.preventDefault();const message=prompt.value.trim();if(!message)return;turn(message,'user');prompt.value='';send.disabled=true;status.textContent='Consultando Grok e pesquisando na web…';try{const r=await fetch('./api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message,history:history.slice(-12)})});if(!r.ok)throw Error(`Erro HTTP ${r.status}`);const d=await r.json();const div=turn(d.answer||'Sem texto na resposta.','assistant');formatAnswer(div,d.answer||'Sem texto na resposta.');const meta=document.createElement('div');meta.className='meta';meta.textContent=`${d.seconds}s · ${d.web_search_calls.length} chamadas de busca · ${d.citations.length} citações`;div.append(meta);for(const c of d.citations){try{const url=new URL(c.url);if(!['https:','http:'].includes(url.protocol))continue;const a=document.createElement('a');a.href=url.href;a.target='_blank';a.rel='noopener noreferrer';a.textContent=c.title && c.title!=='1'?c.title:url.hostname;div.append(document.createElement('br'),a);}catch{}}const details=document.createElement('details');const summary=document.createElement('summary');summary.textContent='Evidências da ferramenta';const pre=document.createElement('pre');pre.textContent=JSON.stringify({response_id:d.response_id,usage:d.usage,web_search_calls:d.web_search_calls},null,2);details.append(summary,pre);div.append(details);history.push({role:'user',content:message},{role:'assistant',content:d.answer});status.textContent='Pesquisa concluída';}catch(err){turn(err.message,'assistant');status.textContent='Não foi possível concluir';}finally{send.disabled=false;prompt.focus();}}); form.addEventListener('submit',async e=>{e.preventDefault();const message=prompt.value.trim();if(!message)return;turn(message,'user');prompt.value='';send.disabled=true;status.textContent='Consultando Grok e pesquisando na web…';try{const r=await fetch('./api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message,history:history.slice(-12)})});if(!r.ok)throw Error(`Erro HTTP ${r.status}`);const d=await r.json();const div=turn(d.answer||'Sem texto na resposta.','assistant');formatAnswer(div,d.answer||'Sem texto na resposta.');const meta=document.createElement('div');meta.className='meta';meta.textContent=`${d.seconds}s · ${d.web_search_calls.length} chamadas de busca · ${d.citations.length} citações`;div.append(meta);for(const c of d.citations){try{const url=new URL(c.url);if(!['https:','http:'].includes(url.protocol))continue;const a=document.createElement('a');a.href=url.href;a.target='_blank';a.rel='noopener noreferrer';a.textContent=c.title && c.title!=='1'?c.title:url.hostname;div.append(document.createElement('br'),a);}catch{}}const details=document.createElement('details');const summary=document.createElement('summary');summary.textContent='Evidências da ferramenta';const pre=document.createElement('pre');pre.textContent=JSON.stringify({response_id:d.response_id,usage:d.usage,web_search_calls:d.web_search_calls},null,2);details.append(summary,pre);div.append(details);history.push({role:'user',content:message},{role:'assistant',content:d.answer});status.textContent='Pesquisa concluída';}catch(err){turn(err.message,'assistant');status.textContent='Não foi possível concluir';}finally{send.disabled=false;prompt.focus();}});
fetch('./health').then(r=>r.json()).then(d=>{document.querySelector('.badge').textContent='OCI · '+d.model.replace('xai.','');}).catch(()=>{});

112
test_models.py Normal file
View File

@@ -0,0 +1,112 @@
"""Teste pago da mesma aplicação ASGI com modelos Grok via OCI.
Executar com variáveis de .env exportadas. Concorrência máxima: 2.
Uma chamada por modelo, sem retries; até 2400 tokens de saída e timeout de 150s.
"""
import argparse
import concurrent.futures
import importlib.metadata
from datetime import datetime
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import time
from zoneinfo import ZoneInfo
MODELS = [
'xai.grok-4.20-0309-non-reasoning', 'xai.grok-4.20-0309-reasoning',
'xai.grok-4.20-non-reasoning', 'xai.grok-4.20-reasoning',
'xai.grok-4.3', 'xai.grok-4.6', 'xai.grok-4.7',
]
ROOT = Path(__file__).resolve().parent
def worker(model):
os.environ['OCI_MODEL'] = model
from fastapi.testclient import TestClient
import app
captured = {}
original = app.requests.post
def observe(*args, **kwargs):
response = original(*args, **kwargs)
captured['http'] = response.status_code
captured['body'] = response.json()
return response
app.requests.post = observe
now = datetime.now(ZoneInfo('America/Sao_Paulo'))
message = (f'Use web_search para encontrar uma notícia publicada hoje ({now.date()}). '
'Faça uma consulta curta e cite uma fonte com URL e data. Responda em até 100 palavras. '
'Se não encontrar notícia de hoje, declare isso.')
started = time.monotonic()
with TestClient(app.app) as client:
health = client.get('/health').json()
response = client.post('/api/chat', auth=('poc', os.environ['APP_PASSWORD']), json={'message': message})
data = response.json()
calls = data.get('web_search_calls', [])
completed = [c for c in calls if c.get('status') == 'completed']
# Exigir ação real de pesquisa, fontes e citações: HTTP 200 sozinho não basta.
searches = [c for c in completed if c.get('action', {}).get('type') == 'search']
sources = [s for c in searches for s in c.get('action', {}).get('sources', [])]
confirmed = response.status_code == 200 and bool(searches) and bool(sources) and bool(data.get('citations'))
body = captured.get('body', {})
error = body.get('error', {})
error_text = str(error.get('message', ''))
for key in ('OCI_API_KEY', 'OCI_PROJECT_ID', 'APP_PASSWORD'):
value = os.environ.get(key)
if value: error_text = error_text.replace(value, '[redacted]')
error_text = re.sub(r'ocid1\.[\w.-]+', '[redacted]', error_text)
row = {'requested_model': model, 'returned_model': data.get('model'), 'tested_at': now.isoformat(),
'app_http': response.status_code, 'oci_http': captured.get('http'),
'seconds': round(time.monotonic()-started, 2), 'websearch_confirmed': confirmed,
'completed_searches': len(searches), 'source_count': len(sources),
'citation_count': len(data.get('citations', [])),
'tool_events': [{'status': c.get('status'), 'action': c.get('action', {}).get('type')} for c in calls],
'usage': data.get('usage'),
'citation_urls': sorted({c['url'] for c in data.get('citations', []) if c.get('url')}),
'error': error_text or data.get('detail'), 'health_model': health['model']}
folder = ROOT/'evidence'/'model-tests-raw'
folder.mkdir(parents=True, exist_ok=True)
(folder/(model+'.json')).write_text(json.dumps({'message': message, 'app_response': data, 'upstream': captured}, ensure_ascii=False, indent=2))
print(json.dumps(row, ensure_ascii=False))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--worker', choices=MODELS)
parser.add_argument('--models', nargs='+', choices=MODELS, default=MODELS)
args = parser.parse_args()
if args.worker:
worker(args.worker)
return
def run(model):
result = subprocess.run([sys.executable, str(Path(__file__).resolve()), '--worker', model],
capture_output=True, text=True, timeout=180, cwd=ROOT)
if result.returncode:
raise RuntimeError(f'{model}: worker failed (exit {result.returncode})')
row = json.loads(result.stdout)
print(f"{model}: app={row['app_http']} OCI={row['oci_http']} search={row['websearch_confirmed']} {row['seconds']}s", flush=True)
return row
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
rows = list(pool.map(run, args.models))
previous = ROOT/'evidence/grok-websearch-models.json'
if previous.exists():
old = json.loads(previous.read_text())
today = datetime.now(ZoneInfo('America/Sao_Paulo')).date().isoformat()
if old.get('tested_at', '').startswith(today):
rows += [row for row in old['results'] if row['requested_model'] not in args.models]
rows.sort(key=lambda row: MODELS.index(row['requested_model']))
report = {'tested_at': datetime.now(ZoneInfo('America/Sao_Paulo')).isoformat(),
'region': 'us-chicago-1', 'endpoint': '/v1/responses',
'method': 'POST /api/chat via FastAPI TestClient; inferência OCI real; um processo isolado por modelo',
'test_runtime': {'python': sys.version.split()[0], **{name: importlib.metadata.version(name) for name in ['fastapi', 'starlette', 'httpx', 'requests']}},
'max_output_tokens': 2400, 'concurrency': 2, 'retries': 0,
'confirmation_criteria': 'HTTP 200, search completed, fontes não vazias e citações estruturadas',
'results': rows,
'aggregate_usage': {key: sum((r.get('usage') or {}).get(key, 0) or 0 for r in rows)
for key in ['input_tokens', 'output_tokens', 'total_tokens', 'num_server_side_tools_used', 'cost_in_usd_ticks']}}
(ROOT/'evidence').mkdir(exist_ok=True)
(ROOT/'evidence/grok-websearch-models.json').write_text(json.dumps(report, ensure_ascii=False, indent=2)+'\n')
if __name__ == '__main__':
main()